Files
krow_backend/go-api/internal/httpserver/worker_with_role_test.go
Aravind cf99866e12
Some checks failed
CI / test (push) Failing after 4m38s
CI / fixture (push) Failing after 9s
create employee table
2026-09-05 10:44:47 +05:30

176 lines
6.5 KiB
Go

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