package collab import ( "os" "strings" "testing" ) // The entity vocabulary must match the CHECK constraint in migration 0087 exactly. If the two drift, a type // the handler accepts is refused by the database at insert time, which surfaces as a 500 on a claim rather // than a 404 on an unknown type. func TestEntityVocabularyMatchesTheMigration(t *testing.T) { sql, err := os.ReadFile("../../../db/migrations/0087_org_settings.sql") if err != nil { t.Fatalf("read migration: %v", err) } // Both work_claims and work_touches declare the same list; checking against the file catches a change to // either without needing a database. for _, typ := range EntityTypes() { if !strings.Contains(string(sql), "'"+typ+"'") { t.Errorf("entity type %q is accepted by the module but is not in the migration's CHECK", typ) } } // And the reverse: nothing in the migration's list may be missing here, or a record type exists in the // schema that no route can reach. for _, typ := range []string{"candidate", "requisition", "opportunity", "proposal", "client"} { if !ValidEntityType(typ) { t.Errorf("the migration allows %q and the module does not", typ) } } } // The table name is interpolated into SQL, so the map it comes from is the only thing standing between this // and an injection. This asserts the shape rather than the behaviour: every value must be a bare identifier. func TestEntityTableNamesAreBareIdentifiers(t *testing.T) { for typ, table := range entityTables { if table == "" { t.Errorf("%q maps to an empty table name", typ) } for _, r := range table { if (r < 'a' || r > 'z') && r != '_' { t.Errorf("table name %q for %q is not a bare lowercase identifier, it must never reach SQL", table, typ) } } } for typ, col := range ownerColumns { if !ValidEntityType(typ) { t.Errorf("ownerColumns names %q which is not a valid entity type", typ) } for _, r := range col { if (r < 'a' || r > 'z') && r != '_' { t.Errorf("owner column %q for %q is not a bare lowercase identifier", col, typ) } } } } // An unknown type must be refused BEFORE any query is built, which is what makes the interpolation safe. func TestUnknownEntityTypeRefused(t *testing.T) { for _, bad := range []string{"", "user", "organization", "candidates", "CANDIDATE", "candidate; drop table users", "candidate'"} { if ValidEntityType(bad) { t.Errorf("%q must not be a valid entity type", bad) } } } // The activity vocabulary must also match the migration, for the same reason as the entity vocabulary. func TestActivityVocabularyMatchesTheMigration(t *testing.T) { sql, err := os.ReadFile("../../../db/migrations/0087_org_settings.sql") if err != nil { t.Fatalf("read migration: %v", err) } for a := range validActivities { if !strings.Contains(string(sql), "'"+a+"'") { t.Errorf("activity %q is accepted by the module but is not in the migration's CHECK", a) } } } func TestEntityTypesIsSortedAndComplete(t *testing.T) { got := EntityTypes() if len(got) != len(entityTables) { t.Fatalf("EntityTypes returned %d of %d types", len(got), len(entityTables)) } for i := 1; i < len(got); i++ { if got[i] < got[i-1] { t.Errorf("EntityTypes is not sorted: %q came before %q", got[i-1], got[i]) } } if !strings.Contains(typeList(), "candidate") { t.Error("the human readable type list is missing candidate") } } // THE STRUCTURAL GUARANTEE. activityTx is the ONLY place the visibility rule is applied, and every read path // must funnel through it. A future read that queried work_claims directly would return another recruiter's // activity to somebody the organization said must not see it. func TestEveryReadGoesThroughActivityTx(t *testing.T) { src, err := os.ReadFile("service.go") if err != nil { t.Fatalf("read source: %v", err) } s := string(src) if !strings.Contains(s, "security.CanSeeOthersWork(role, st.WorkVisibility)") { t.Fatal("activityTx no longer consults the visibility rule") } // Exactly one call site. More than one means the decision has been copied, and a copy is a place to // forget. if n := strings.Count(s, "security.CanSeeOthersWork("); n != 1 { t.Errorf("the visibility rule is consulted in %d places, it must be exactly one", n) } // MyClaims is the deliberate exception and says so: it returns only the caller's own claims. body := s[strings.Index(s, "func (s *Service) MyClaims("):] if i := strings.Index(body, "\nfunc "); i > 0 { body = body[:i] } if !strings.Contains(body, "c.user_id = $1") { t.Error("MyClaims must filter to the caller's own claims, it is the one read that skips the visibility rule") } } // Presence must be an upsert, never an append. An append generates a row per page view per recruiter per // candidate and turns a presence feature into a browsing history of a named employee. func TestPresenceIsAnUpsertNotAnAppend(t *testing.T) { src, err := os.ReadFile("service.go") if err != nil { t.Fatalf("read source: %v", err) } s := string(src) if !strings.Contains(s, "ON CONFLICT (organization_id, entity_type, entity_id, user_id)") { t.Error("work_touches is no longer upserted; presence would accumulate a row per view") } if !strings.Contains(s, "st.ShowActivityPresence") { t.Error("presence is recorded without checking whether the organization wants it") } } // An expired claim must be released before a new one is attempted, or the partial unique index makes an // abandoned claim own a record permanently. func TestExpiredClaimsAreReleasedBeforeClaiming(t *testing.T) { src, err := os.ReadFile("service.go") if err != nil { t.Fatalf("read source: %v", err) } body := string(src)[strings.Index(string(src), "func (s *Service) TakeClaim("):] if i := strings.Index(body, "\nfunc "); i > 0 { body = body[:i] } if !strings.Contains(body, "released_at IS NULL AND expires_at <= now()") { t.Error("TakeClaim does not release expired claims first; an abandoned claim would own a record forever") } if !strings.Contains(body, "assertExists(ctx, tx, entityType, entityID)") { t.Error("TakeClaim does not prove the record exists in this tenant before writing about it") } }