90 lines
3.2 KiB
Go
90 lines
3.2 KiB
Go
package httpserver_test
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
// Who counts as the same person.
|
|
//
|
|
// The rule is the schema's and it is worth stating plainly, because the whole
|
|
// duplicate question turns on it: `worker_profiles` carries
|
|
// UNIQUE (org_id, email) and `email` is `citext`. So identity is the pair
|
|
// (organization, email), compared case-insensitively, and `full_name` carries
|
|
// NO uniqueness at all — an organization may employ any number of people with
|
|
// the same name, and they are different people.
|
|
//
|
|
// These are database guarantees rather than application checks, which is what
|
|
// makes them hold under concurrency: two simultaneous creates of the same
|
|
// identity cannot both win, whatever the callers checked first.
|
|
|
|
func createWorker(t *testing.T, r *rbac, act actor, name, email string) response {
|
|
t.Helper()
|
|
return r.as(act, "POST", "/api/v1/worker-profiles", map[string]any{
|
|
"full_name": name, "email": email,
|
|
})
|
|
}
|
|
|
|
// A name is not an identity. Two people who share one are two records.
|
|
func TestWorkersMayShareAName(t *testing.T) {
|
|
r := newRBAC(t)
|
|
const shared = "Shared Name"
|
|
|
|
first := createWorker(t, r, r.admin, shared, "shared-name-1@example.test")
|
|
second := createWorker(t, r, r.admin, shared, "shared-name-2@example.test")
|
|
|
|
for i, got := range []response{first, second} {
|
|
if got.code != http.StatusCreated {
|
|
t.Fatalf("create %d: %d (%v) — sharing a name must not block creation", i+1, got.code, got.body)
|
|
}
|
|
}
|
|
a := first.body["data"].(map[string]any)
|
|
b := second.body["data"].(map[string]any)
|
|
if a["id"] == b["id"] {
|
|
t.Fatal("two people sharing a name collapsed into one record")
|
|
}
|
|
if a["email"] == b["email"] {
|
|
t.Error("the second worker took the first one's email")
|
|
}
|
|
}
|
|
|
|
// The same identity cannot be created twice, whoever it claims to be, and the
|
|
// refusal is a conflict a caller can act on rather than a 500.
|
|
func TestTheSameIdentityCannotBeCreatedTwice(t *testing.T) {
|
|
r := newRBAC(t)
|
|
const email = "one-identity@example.test"
|
|
|
|
if got := createWorker(t, r, r.admin, "Person One", email); got.code != http.StatusCreated {
|
|
t.Fatalf("first create: %d (%v)", got.code, got.body)
|
|
}
|
|
|
|
for _, tc := range []struct{ name, who, email string }{
|
|
{"a different name on the same email", "Person Two", email},
|
|
{"the same email in another case", "Person Three", "ONE-IDENTITY@EXAMPLE.TEST"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := createWorker(t, r, r.admin, tc.who, tc.email)
|
|
if got.code != http.StatusConflict {
|
|
t.Errorf("= %d, want 409 — the identity is already taken", got.code)
|
|
}
|
|
if got.errCode(t) != "conflict" {
|
|
t.Errorf("error code = %q, want conflict", got.errCode(t))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The identity is scoped to the organization, so the same email in another
|
|
// tenant is another person and is allowed.
|
|
func TestTheSameEmailInAnotherOrganizationIsAnotherPerson(t *testing.T) {
|
|
r := newRBAC(t)
|
|
const email = "cross-tenant-identity@example.test"
|
|
|
|
if got := createWorker(t, r, r.admin, "Inside", email); got.code != http.StatusCreated {
|
|
t.Fatalf("create inside: %d (%v)", got.code, got.body)
|
|
}
|
|
if got := createWorker(t, r, r.outsider, "Outside", email); got.code != http.StatusCreated {
|
|
t.Errorf("create in another organization = %d, want 201 — identity is (org, email)", got.code)
|
|
}
|
|
}
|