create employee table
This commit is contained in:
89
go-api/internal/httpserver/worker_identity_test.go
Normal file
89
go-api/internal/httpserver/worker_identity_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user