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) } }