package httpserver_test import ( "net/http" "testing" ) // Recording a NEW person and their first declared role, atomically. // // The flow this endpoint exists for is a CREATION: HR is adding somebody the // organization does not have yet. So the properties under test are about // creation, not lookup — no worker id is accepted, no name is searched, and the // email the caller states is the identity the row is keyed on. func createWorkerWithRole(t *testing.T, r *rbac, act actor, body map[string]any) response { t.Helper() return r.as(act, "POST", "/api/v1/worker-profiles/with-role", body) } // The happy path, and the two records it must leave behind. func TestCreateWorkerWithRoleCreatesBoth(t *testing.T) { r := newRBAC(t) const email = "new-person@example.test" got := createWorkerWithRole(t, r, r.empA, map[string]any{ "full_name": "New Person", "email": email, "role": map[string]any{ "role_category": "Bartender", "experience_years": 3, "english_level": "fluent", "certifications": []string{"A Certification"}, "desired_pay_min": 30, "desired_pay_max": 40, "availability": []string{"Weekdays"}, "notes": "recorded by the panel", }, }) if got.code != http.StatusCreated { t.Fatalf("= %d, want 201 (%v)", got.code, got.body) } data := got.body["data"].(map[string]any) worker := data["worker"].(map[string]any) role := data["role"].(map[string]any) if worker["id"] == nil || worker["id"] == "" { t.Fatal("no worker id came back") } // The whole point: the role points at the worker this call created. if role["worker_profile_id"] != worker["id"] { t.Errorf("role.worker_profile_id = %v, want the new worker %v", role["worker_profile_id"], worker["id"]) } if role["worker_email"] != worker["email"] { t.Errorf("role.worker_email = %v, want %v", role["worker_email"], worker["email"]) } // The operator is the author, never the subject. if role["created_by"] != r.empA.id { t.Errorf("created_by = %v, want the operator %v", role["created_by"], r.empA.id) } if worker["email"] == r.empA.email { t.Fatal("the operator became the worker") } // The role's own fields survived, and did not land on the worker. if role["role_category"] != "Bartender" { t.Errorf("role_category = %v", role["role_category"]) } if _, leaked := worker["role_category"]; leaked { t.Error("a role field landed on the worker record") } // Both are readable afterwards, under the caller's own org predicate. if !r.ids(t, r.empA, "/api/v1/worker-profiles")[worker["id"].(string)] { t.Error("the new worker is missing from the worker listing") } if !r.ids(t, r.empA, "/api/v1/employee-roles")[role["id"].(string)] { t.Error("the new role is missing from the role listing") } } // A name is not an identity: same name, different emails, two people. func TestCreateWorkerWithRoleAllowsARepeatedName(t *testing.T) { r := newRBAC(t) const name = "Repeated Name" first := createWorkerWithRole(t, r, r.admin, map[string]any{ "full_name": name, "email": "repeat-1@example.test", "role": map[string]any{"role_category": "Server"}, }) second := createWorkerWithRole(t, r, r.admin, map[string]any{ "full_name": name, "email": "repeat-2@example.test", "role": map[string]any{"role_category": "Chef"}, }) for i, got := range []response{first, second} { if got.code != http.StatusCreated { t.Fatalf("create %d = %d (%v) — a shared name must not block creation", i+1, got.code, got.body) } } a := first.body["data"].(map[string]any)["worker"].(map[string]any) b := second.body["data"].(map[string]any)["worker"].(map[string]any) if a["id"] == b["id"] { t.Fatal("two people sharing a name collapsed into one record") } } // The identity is the email, and the database decides. A repeat is refused and // leaves NOTHING behind — no worker, no role. func TestCreateWorkerWithRoleRollsBackOnDuplicateIdentity(t *testing.T) { r := newRBAC(t) const email = "taken-identity@example.test" if got := createWorkerWithRole(t, r, r.admin, map[string]any{ "full_name": "First Person", "email": email, "role": map[string]any{"role_category": "Server"}, }); got.code != http.StatusCreated { t.Fatalf("first create: %d (%v)", got.code, got.body) } before := len(r.ids(t, r.admin, "/api/v1/employee-roles")) got := createWorkerWithRole(t, r, r.admin, map[string]any{ "full_name": "Second Person", "email": email, "role": map[string]any{"role_category": "Chef"}, }) if got.code != http.StatusConflict { t.Fatalf("duplicate identity = %d, want 409 (%v)", got.code, got.body) } if after := len(r.ids(t, r.admin, "/api/v1/employee-roles")); after != before { t.Errorf("%d roles after a refused create, want %d — the transaction did not roll back", after, before) } } // A role cannot be recorded for nobody, and an email is never invented for a // name that arrived without one. func TestCreateWorkerWithRoleRequiresBothNameAndEmail(t *testing.T) { r := newRBAC(t) for _, tc := range []struct { name string body map[string]any }{ {"no email", map[string]any{"full_name": "Nameless Email", "role": map[string]any{"role_category": "Server"}}}, {"blank email", map[string]any{"full_name": "Blank", "email": " ", "role": map[string]any{"role_category": "Server"}}}, {"no name", map[string]any{"email": "no-name@example.test", "role": map[string]any{"role_category": "Server"}}}, {"neither", map[string]any{"role": map[string]any{"role_category": "Server"}}}, } { t.Run(tc.name, func(t *testing.T) { got := createWorkerWithRole(t, r, r.admin, tc.body) if got.code == http.StatusCreated { t.Fatalf("accepted a worker with %s: %v", tc.name, got.body) } }) } } // Talent cannot record workers, and another organization cannot see the ones // this one records. func TestCreateWorkerWithRoleIsScopedAndAuthorized(t *testing.T) { r := newRBAC(t) if got := createWorkerWithRole(t, r, r.talA, map[string]any{ "full_name": "Not Allowed", "email": "not-allowed@example.test", "role": map[string]any{"role_category": "Server"}, }); got.code != http.StatusForbidden { t.Errorf("talent create = %d, want 403", got.code) } made := createWorkerWithRole(t, r, r.admin, map[string]any{ "full_name": "Inside Only", "email": "inside-only@example.test", "role": map[string]any{"role_category": "Server"}, }) if made.code != http.StatusCreated { t.Fatalf("create: %d (%v)", made.code, made.body) } roleID := made.body["data"].(map[string]any)["role"].(map[string]any)["id"].(string) if r.ids(t, r.outsider, "/api/v1/employee-roles")[roleID] { t.Error("a role leaked into another organization") } }