aravind changes

This commit is contained in:
2026-08-25 16:37:05 +05:30
parent cadea4bd92
commit b6f8655909
27 changed files with 5058 additions and 163 deletions

View File

@@ -329,3 +329,326 @@ func TestWorkflowEndpointsRequireASession(t *testing.T) {
}
}
}
/* ── Activity vocabulary ────────────────────────────────────────────────── */
// activityTypes returns the event types written about one worker, newest first.
//
// Read straight from the table rather than through GET /user-activity so the
// assertion is about what was STORED. The frontend's anomaly detection reads
// these strings — PRIVILEGED_EVENTS is ['hire_candidate', 'create_position'] —
// and a value the vocabulary does not contain is not a different label, it is
// an event that silently stops counting.
func activityTypes(t *testing.T, r *rbac, workerEmail string) []string {
t.Helper()
rows, err := r.h.Pool.Query(context.Background(),
`SELECT event_type FROM user_activity
WHERE org_id = $1::uuid AND worker_email = $2::citext
ORDER BY created_date DESC, id DESC`, r.orgID, workerEmail)
if err != nil {
t.Fatalf("read user_activity: %v", err)
}
defer rows.Close()
var out []string
for rows.Next() {
var s string
if err := rows.Scan(&s); err != nil {
t.Fatalf("scan user_activity: %v", err)
}
out = append(out, s)
}
if err := rows.Err(); err != nil {
t.Fatalf("read user_activity: %v", err)
}
return out
}
func TestHireWritesTheFrontendsActivityEvent(t *testing.T) {
r := newRBAC(t)
app := applicationFor(t, r, r.activePosting, "Evented", "evented@example.test")
if got := r.as(r.admin, "POST", "/api/v1/job-applications/"+app+"/hire",
map[string]any{}); got.code != http.StatusCreated {
t.Fatalf("hire: got %d, want 201 (%v)", got.code, got.body)
}
events := activityTypes(t, r, "evented@example.test")
if len(events) != 1 || events[0] != "hire_candidate" {
t.Errorf("activity = %v, want exactly [hire_candidate] — the vocabulary "+
"activitySignals.js reads, and the one the seed fixture uses", events)
}
}
func TestAssignWritesTheFrontendsActivityEvent(t *testing.T) {
r := newRBAC(t)
if got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{
{"worker_email": "evented-assign@example.test", "worker_name": "Evented",
"starts_at": "2026-09-01T09:00:00Z"},
}}); got.code != http.StatusCreated {
t.Fatalf("assign: got %d, want 201 (%v)", got.code, got.body)
}
events := activityTypes(t, r, "evented-assign@example.test")
if len(events) != 1 || events[0] != "assign_employee" {
t.Errorf("activity = %v, want exactly [assign_employee]", events)
}
}
/* ── Assign: source ─────────────────────────────────────────────────────── */
// An unspecified source must mean what the column says it means. The default in
// 000001 is `owliver` and the frontend sends `owliver`; substituting `manual`
// made a row written through this endpoint disagree with a row written through
// POST /assignments about where the same action came from.
func TestAssignDefaultsSourceToTheColumnDefault(t *testing.T) {
r := newRBAC(t)
got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{
{"worker_email": "default-source@example.test", "starts_at": "2026-09-01T09:00:00Z"},
{"worker_email": "explicit-source@example.test", "starts_at": "2026-09-01T09:00:00Z",
"source": "manual"},
}})
if got.code != http.StatusCreated {
t.Fatalf("assign: got %d, want 201 (%v)", got.code, got.body)
}
created := assignmentsOf(t, got)
if created[0]["source"] != "owliver" {
t.Errorf("source = %v, want owliver", created[0]["source"])
}
// An explicit value is still the caller's.
if created[1]["source"] != "manual" {
t.Errorf("source = %v, want the supplied manual", created[1]["source"])
}
}
// assignmentsOf reads the assignment records out of an assign response.
func assignmentsOf(t *testing.T, got response) []map[string]any {
t.Helper()
data, _ := got.body["data"].(map[string]any)
raw, _ := data["assignments"].([]any)
if raw == nil {
t.Fatalf("response carries no assignments: %v", got.body)
}
out := make([]map[string]any, 0, len(raw))
for _, rec := range raw {
out = append(out, rec.(map[string]any))
}
return out
}
// applicationByID reads one application as an operator, or fails.
func applicationByID(t *testing.T, r *rbac, id string) map[string]any {
t.Helper()
list := r.as(r.admin, "GET", "/api/v1/job-applications?limit=500", nil)
if list.code != http.StatusOK {
t.Fatalf("list applications: %d (%v)", list.code, list.body)
}
for _, raw := range list.body["data"].([]any) {
rec := raw.(map[string]any)
if rec["id"] == id {
return rec
}
}
t.Fatalf("application %s not found", id)
return nil
}
/* ── Assign: the application a worker does not have yet ─────────────────── */
// The behaviour the frontend had and the endpoint did not.
//
// An application is what puts a person in the pipeline for a role: the
// candidate record is addressed by it and an interview takes one as its
// subject. A worker assigned from the talent pool has none, so the endpoint has
// to file one — in the same transaction as the assignment, which is the half
// the frontend could not do.
func TestAssignCreatesTheApplicationItNeeds(t *testing.T) {
r := newRBAC(t)
appsBefore := countRows(t, r, "job_applications")
got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{{
"worker_email": "from-pool@example.test",
"worker_name": "Pool Worker",
"starts_at": "2026-09-01T09:00:00Z",
"match_score": 88,
"application": map[string]any{
"job_title": "Open Role",
"phone": "555-0100",
"years_experience": 4,
"skills": []string{"service", "bar"},
"professional_summary": "Placed from the talent pool.",
"ai_score": 88,
},
}}})
if got.code != http.StatusCreated {
t.Fatalf("assign: got %d, want 201 (%v)", got.code, got.body)
}
if after := countRows(t, r, "job_applications"); after != appsBefore+1 {
t.Fatalf("job_applications: %d -> %d, want exactly one more", appsBefore, after)
}
assignment := assignmentsOf(t, got)[0]
linked, _ := assignment["application_id"].(string)
if linked == "" {
t.Fatal("the assignment was not linked to the application that was created for it")
}
app := applicationByID(t, r, linked)
if app["status"] != "assigned" {
t.Errorf("application.status = %v, want assigned", app["status"])
}
if app["email"] != "from-pool@example.test" {
t.Errorf("application.email = %v, want the worker's email", app["email"])
}
if app["job_posting_id"] != r.activePosting {
t.Errorf("application.job_posting_id = %v, want the posting being assigned to", app["job_posting_id"])
}
// applicant_name falls back to the worker's name rather than being blank —
// the column has a not-blank check.
if app["applicant_name"] != "Pool Worker" {
t.Errorf("application.applicant_name = %v, want the worker's name", app["applicant_name"])
}
if app["phone"] != "555-0100" {
t.Errorf("application.phone = %v, want the supplied phone", app["phone"])
}
if score, ok := app["ai_score"].(float64); !ok || int(score) != 88 {
t.Errorf("application.ai_score = %v, want 88", app["ai_score"])
}
// The audit entry names the application, so the feed can open it.
var activityApp *string
if err := r.h.Pool.QueryRow(context.Background(),
`SELECT application_id::text FROM user_activity
WHERE org_id = $1::uuid AND worker_email = $2::citext`,
r.orgID, "from-pool@example.test").Scan(&activityApp); err != nil {
t.Fatalf("read the activity entry: %v", err)
}
if activityApp == nil || *activityApp != linked {
t.Errorf("activity.application_id = %v, want %s", activityApp, linked)
}
}
// (job_posting_id, email) is UNIQUE, so the second assign of the same person to
// the same posting must find the application rather than try to file another —
// and the comparison is case-insensitive, because the column is citext and the
// frontend's own lookup lowercased both sides.
func TestAssignLinksAnExistingApplicationInsteadOfDuplicating(t *testing.T) {
r := newRBAC(t)
existing := applicationFor(t, r, r.activePosting, "Already Applied", "Already.Applied@example.test")
appsBefore := countRows(t, r, "job_applications")
got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{{
"worker_email": "already.applied@example.test",
"worker_name": "Already Applied",
"starts_at": "2026-09-01T09:00:00Z",
"application": map[string]any{"job_title": "Open Role"},
}}})
if got.code != http.StatusCreated {
t.Fatalf("assign: got %d, want 201 (%v)", got.code, got.body)
}
if after := countRows(t, r, "job_applications"); after != appsBefore {
t.Errorf("job_applications: %d -> %d, want no new row for a person who already applied",
appsBefore, after)
}
if linked := assignmentsOf(t, got)[0]["application_id"]; linked != existing {
t.Errorf("assignment.application_id = %v, want the existing application %s", linked, existing)
}
if app := applicationByID(t, r, existing); app["status"] != "assigned" {
t.Errorf("application.status = %v, want assigned", app["status"])
}
}
// An id the caller already has still wins over the payload: it is a decision
// they have made, and honouring the payload instead could file a second
// application for the same placement.
func TestAssignPrefersTheSuppliedApplicationID(t *testing.T) {
r := newRBAC(t)
app := applicationFor(t, r, r.activePosting, "Named", "named@example.test")
appsBefore := countRows(t, r, "job_applications")
got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{{
"worker_email": "named@example.test",
"worker_name": "Named",
"starts_at": "2026-09-01T09:00:00Z",
"application_id": app,
"application": map[string]any{"applicant_name": "Ignored"},
}}})
if got.code != http.StatusCreated {
t.Fatalf("assign: got %d, want 201 (%v)", got.code, got.body)
}
if after := countRows(t, r, "job_applications"); after != appsBefore {
t.Errorf("job_applications: %d -> %d, want no new row", appsBefore, after)
}
if linked := assignmentsOf(t, got)[0]["application_id"]; linked != app {
t.Errorf("assignment.application_id = %v, want %s", linked, app)
}
if stored := applicationByID(t, r, app); stored["applicant_name"] != "Named" {
t.Errorf("applicant_name = %v — the payload overwrote a named application",
stored["applicant_name"])
}
}
// No payload, no application. A worker placed straight from the workforce is
// legitimate, and one must not be invented for them.
func TestAssignWithoutAnApplicationPayloadLinksNothing(t *testing.T) {
r := newRBAC(t)
appsBefore := countRows(t, r, "job_applications")
got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{
{"worker_email": "unattached@example.test", "worker_name": "Unattached",
"starts_at": "2026-09-01T09:00:00Z"},
}})
if got.code != http.StatusCreated {
t.Fatalf("assign: got %d, want 201 (%v)", got.code, got.body)
}
if after := countRows(t, r, "job_applications"); after != appsBefore {
t.Errorf("job_applications: %d -> %d, want no application invented", appsBefore, after)
}
if linked := assignmentsOf(t, got)[0]["application_id"]; linked != nil {
t.Errorf("assignment.application_id = %v, want null", linked)
}
}
// The application payload is validated exactly as POST /job-applications would
// validate it, and the batch element that produced the complaint is named.
func TestAssignValidatesTheApplicationPayload(t *testing.T) {
r := newRBAC(t)
assignBefore := countRows(t, r, "assignments")
appsBefore := countRows(t, r, "job_applications")
got := r.as(r.admin, "POST", "/api/v1/job-postings/"+r.activePosting+"/assignments",
map[string]any{"workers": []map[string]any{
{"worker_email": "good@example.test", "worker_name": "Good",
"starts_at": "2026-09-01T09:00:00Z",
"application": map[string]any{"job_title": "Open Role"}},
{"worker_email": "bad@example.test", "worker_name": "Bad",
"starts_at": "2026-09-01T09:00:00Z",
"application": map[string]any{"english_level": "telepathic"}},
}})
if got.code != http.StatusUnprocessableEntity {
t.Fatalf("assign with an invalid application: got %d, want 422 (%v)", got.code, got.body)
}
details, _ := got.body["error"].(map[string]any)["details"].(map[string]any)
if details["workers[1].english_level"] == nil {
t.Errorf("details = %v, want the failure attributed to workers[1]", details)
}
// And the first worker — whose application WAS filed before the second
// failed — must be gone with it.
if after := countRows(t, r, "job_applications"); after != appsBefore {
t.Errorf("job_applications: %d -> %d — a rejected batch left an application behind",
appsBefore, after)
}
if after := countRows(t, r, "assignments"); after != assignBefore {
t.Errorf("assignments: %d -> %d — a rejected batch left an assignment behind",
assignBefore, after)
}
}