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