//go:build integration package integration import ( "context" "testing" "github.com/google/uuid" "github.com/missionworkforce/missionmatch/internal/modules/recruiting" "github.com/missionworkforce/missionmatch/internal/platform/security" "github.com/missionworkforce/missionmatch/internal/platform/tenant" ) // WARNING. Point MM_APP_DSN and MM_MIGRATOR_DSN at a scratch or staging database, never at production. // seedRoleUser creates a user at a given role and returns a context acting as them. func seedRoleUser(t *testing.T, org uuid.UUID, name, role string) (uuid.UUID, context.Context) { t.Helper() var id uuid.UUID if err := migrator.QueryRow(context.Background(), `INSERT INTO users (organization_id, email, full_name, role, status) VALUES ($1, $2, $3, $4, 'active') RETURNING id`, org, name+"-"+uuid.New().String()[:8]+"@example.com", name, role).Scan(&id); err != nil { t.Fatalf("seed %s: %v", role, err) } ctx := tenant.WithUser(tenant.WithOrg(context.Background(), org.String()), tenant.Principal{UserID: id.String(), Role: role}) return id, ctx } // TestManagerCanReassignADeskAndARecruiterCannot is the behaviour the whole tier exists for, proved // against a real database rather than against the matrix alone. func TestManagerCanReassignADeskAndARecruiterCannot(t *testing.T) { org := seedOrg(t) aliceID, alice := seedRoleUser(t, org, "Alice Holder", "recruiter") _, bob := seedRoleUser(t, org, "Bob Peer", "recruiter") _, manager := seedRoleUser(t, org, "Mia Manager", "manager") svc := reqSvc() client, err := svc.CreateClient(alice, recruiting.CreateClientInput{Name: "Contested Account"}) if err != nil { t.Fatalf("create client: %v", err) } id := uuid.MustParse(client.ID) if client.OwnerName != "Alice Holder" { t.Fatalf("the creating recruiter did not take the account, owner = %q", client.OwnerName) } // A peer recruiter is refused, and the refusal names who holds it. name := "Bob's edit" _, err = svc.UpdateClient(bob, id, recruiting.UpdateClientInput{ ExpectedUpdatedAt: client.UpdatedAt, Name: &name}) var notOwner *recruiting.ErrNotOwner if !asErrNotOwner(err, ¬Owner) { t.Fatalf("a peer recruiter was allowed to edit another recruiter's account: %v", err) } // The manager may reassign it, which before this tier required admin, and admin also carries PII // deletion and account management. moved, err := svc.Reassign(manager, id, aliceID) if err != nil { t.Fatalf("a manager could not reassign a desk: %v", err) } if moved.OwnerUserID != aliceID.String() { t.Errorf("after reassignment the owner is %q", moved.OwnerName) } // And a recruiter still cannot. if _, err := svc.Reassign(bob, id, aliceID); err == nil { t.Error("a recruiter reassigned an account, so the manager tier grants nothing") } } func asErrNotOwner(err error, target **recruiting.ErrNotOwner) bool { if err == nil { return false } e, ok := err.(*recruiting.ErrNotOwner) if ok { *target = e } return ok } // TestManagerCannotReachAdminAuthorityLive. The containment property, exercised through the service // rather than the matrix, because a capability check that is never called protects nothing. func TestManagerCannotReachAdminAuthorityLive(t *testing.T) { org := seedOrg(t) _, manager := seedRoleUser(t, org, "Mia Manager", "manager") svc := reqSvc() // Org settings are owner only. A manager changing the timezone would move what "today" means for // every count in the product. if _, err := svc.SetTimeZone(manager, "America/Chicago"); err == nil { t.Error("a manager changed the organization timezone") } // But the capabilities a manager DOES hold work, or the tier is unusable. if _, err := svc.CreateRequisition(manager, recruiting.CreateRequisitionInput{Title: "Manager role"}); err != nil { t.Errorf("a manager could not open a job order: %v", err) } if _, _, err := svc.Export(manager, recruiting.ExportPipeline, recruiting.PeriodMonth, ""); err != nil { t.Errorf("a manager could not export: %v", err) } } // TestCapabilitiesTravelWithTheSession. The list the browser gates on and the checks the server enforces // come from one matrix, so a screen cannot offer something the API will refuse. func TestCapabilitiesMatchTheServerForEveryRole(t *testing.T) { for _, role := range []security.Role{ security.RoleViewer, security.RoleMember, security.RoleRecruiter, security.RoleManager, security.RoleAdmin, security.RoleOwner, } { caps := security.CapabilitiesOf(role) set := map[string]bool{} for _, c := range caps { set[c] = true } for _, cap := range []security.Capability{ security.CapDeskReassign, security.CapOrgSettings, security.CapPrivacyErase, security.CapExportRun, security.CapCandidateRead, security.CapUserInvite, } { if set[string(cap)] != security.Has(role, cap) { t.Errorf("role %s: session list and server disagree about %s", role, cap) } } } } // TestManagerRoleIsAcceptedByTheDatabase. The Go vocabulary and the CHECK constraint have to agree, or // promoting someone is a 500. func TestManagerRoleIsAcceptedByTheDatabase(t *testing.T) { org := seedOrg(t) if _, err := migrator.Exec(context.Background(), `INSERT INTO users (organization_id, email, full_name, role, status) VALUES ($1, $2, 'A Manager', 'manager', 'active')`, org, "mgr-"+uuid.New().String()[:8]+"@example.com"); err != nil { t.Fatalf("the database refused the manager role: %v", err) } // And a role the code does not know is still refused, so the constraint has not simply been widened // to anything. if _, err := migrator.Exec(context.Background(), `INSERT INTO users (organization_id, email, full_name, role, status) VALUES ($1, $2, 'A Wizard', 'wizard', 'active')`, org, "wiz-"+uuid.New().String()[:8]+"@example.com"); err == nil { t.Error("the database accepted an unknown role") } } // TestReassignmentIsTenantScoped. A manager in one organization must not be able to hand another // organization's account to anyone. func TestReassignmentIsTenantScoped(t *testing.T) { orgA := seedOrg(t) _, aliceA := seedRoleUser(t, orgA, "Alice A", "recruiter") orgB := seedOrg(t) _, managerB := seedRoleUser(t, orgB, "Manager B", "manager") userB, _ := seedRoleUser(t, orgB, "Recruiter B", "recruiter") svc := reqSvc() client, err := svc.CreateClient(aliceA, recruiting.CreateClientInput{Name: "Tenant A account"}) if err != nil { t.Fatalf("create in A: %v", err) } if _, err := svc.Reassign(managerB, uuid.MustParse(client.ID), userB); err == nil { t.Fatal("a manager reassigned an account belonging to another tenant") } // Control: A's own account is still hers. got, err := svc.GetClient(aliceA, uuid.MustParse(client.ID)) if err != nil { t.Fatalf("read back in A: %v", err) } if got.OwnerName != "Alice A" { t.Errorf("owner = %q after the cross tenant attempt", got.OwnerName) } }