agent build

This commit is contained in:
2026-08-28 12:21:44 +05:30
parent b6f8655909
commit f7df96c973
138 changed files with 24164 additions and 207 deletions

View File

@@ -0,0 +1,210 @@
package tools_test
import (
"context"
"encoding/json"
"testing"
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/testutil"
"github.com/krow/krow-backend/go-api/internal/tools"
)
// freshOrg creates an organization with no history.
//
// The harness seeds a demo tenant that already has activity in it, so these
// tests build their own: an assertion of "exactly 10 events" is only a real
// assertion when the fixture controls every row.
func freshOrg(t *testing.T, h *testutil.Harness, slug string) string {
t.Helper()
var id string
if err := h.Pool.QueryRow(context.Background(),
`INSERT INTO organizations (name, slug) VALUES ($1, $2) RETURNING id::text`,
slug, slug).Scan(&id); err != nil {
t.Fatalf("create org %s: %v", slug, err)
}
return id
}
// seedActivity writes audit rows for two accounts in one org, and one in
// another, so a leak across either boundary is detectable.
func seedActivity(t *testing.T, h *testutil.Harness, mineOrg, otherOrg string) (admin, talent authctx.Identity) {
t.Helper()
ctx := context.Background()
rows := []struct {
org, eventType, email string
n int
}{
{mineOrg, "login", "boss@example.test", 5},
{mineOrg, "hire_candidate", "boss@example.test", 3},
{mineOrg, "login", "worker@example.test", 2},
// A different tenant entirely. Must never appear in either caller's
// counts, shares or account total.
{otherOrg, "login", "outsider@example.test", 40},
{otherOrg, "delete_position", "outsider@example.test", 40},
}
for _, r := range rows {
for i := 0; i < r.n; i++ {
if _, err := h.Pool.Exec(ctx,
`INSERT INTO user_activity (org_id, event_type, user_email, user_name)
VALUES ($1::uuid, $2, $3, 'Someone')`,
r.org, r.eventType, r.email); err != nil {
t.Fatalf("seed activity: %v", err)
}
}
}
return authctx.Identity{UserID: "u1", OrgID: mineOrg, Role: "admin", Email: "boss@example.test"},
authctx.Identity{UserID: "u2", OrgID: mineOrg, Role: "talent", Email: "worker@example.test"}
}
// seeded builds two fresh tenants and fills them. Returns the two callers.
func seeded(t *testing.T, h *testutil.Harness) (admin, talent authctx.Identity) {
t.Helper()
mine := freshOrg(t, h, "mine-co")
other := freshOrg(t, h, "other-co")
return seedActivity(t, h, mine, other)
}
func runBreakdown(t *testing.T, h *testutil.Harness, ident authctx.Identity, args string) tools.Result {
t.Helper()
reg := tools.NewRegistry()
reg.MustRegister(tools.ActivityBreakdown(h.Pool))
return reg.Dispatch(context.Background(),
tools.Context{Principal: ident, RunID: "run_test"},
"activity_breakdown", json.RawMessage(args))
}
func data(t *testing.T, res tools.Result) map[string]any {
t.Helper()
if res.Error != nil {
t.Fatalf("unexpected tool error: %s — %s", res.Error.Code, res.Error.Message)
}
encoded, _ := json.Marshal(res.Data)
var m map[string]any
if err := json.Unmarshal(encoded, &m); err != nil {
t.Fatalf("result was not an object: %v", err)
}
return m
}
func TestActivityBreakdownScopesToTheTenant(t *testing.T) {
h := testutil.New(t)
admin, _ := seeded(t, h)
got := data(t, runBreakdown(t, h, admin, `{}`))
// 10 in this org. The other tenant's 80 must not be counted, and must not
// show up in the share arithmetic either.
if n := got["totalEvents"].(float64); n != 10 {
t.Errorf("totalEvents = %v, want 10 — the other tenant's rows leaked", n)
}
if n := got["activeAccounts"].(float64); n != 2 {
t.Errorf("activeAccounts = %v, want 2", n)
}
// `delete_position` exists only in the other org.
encoded, _ := json.Marshal(got["kinds"])
if string(encoded) != "" && contains(string(encoded), "delete position") {
t.Errorf("an event kind from another tenant appeared: %s", encoded)
}
}
func TestActivityBreakdownScopesTalentToTheirOwnRows(t *testing.T) {
// I1: the agent may read exactly what the caller could read directly. The
// policy scopes a talent caller to their own email, and this asserts the
// scope is applied as a pre-filter — the totals and shares are computed
// over their rows alone.
h := testutil.New(t)
_, talent := seeded(t, h)
got := data(t, runBreakdown(t, h, talent, `{}`))
if n := got["totalEvents"].(float64); n != 2 {
t.Errorf("totalEvents = %v, want 2 — a talent caller must see only their own entries", n)
}
// The leak that post-filtering would produce: the row text is hidden but
// the organization's volume shows through the account count.
if n := got["activeAccounts"].(float64); n != 1 {
t.Errorf("activeAccounts = %v, want 1 — the tenant's account count leaked through the aggregate", n)
}
encoded, _ := json.Marshal(got["kinds"])
if contains(string(encoded), "hire candidate") {
t.Errorf("a talent caller saw an event kind they did not perform: %s", encoded)
}
}
func TestActivityBreakdownDeniesWithoutATenant(t *testing.T) {
h := testutil.New(t)
res := runBreakdown(t, h, authctx.Identity{UserID: "u", Role: "admin", Email: "x@example.test"}, `{}`)
if res.Error == nil || res.Error.Code != tools.CodeDenied {
t.Fatalf("want a denial, got %+v", res)
}
// §8: the denial must not reveal whether anything exists.
if contains(res.Error.Message, "activity") || contains(res.Error.Message, "org") {
t.Errorf("the denial described what was refused: %q", res.Error.Message)
}
}
func TestActivityBreakdownDeniesAnUnknownRole(t *testing.T) {
// Deny by default: a role the policy table does not list permits nothing.
h := testutil.New(t)
res := runBreakdown(t, h,
authctx.Identity{UserID: "u", OrgID: h.OrgID, Role: "superuser", Email: "x@example.test"}, `{}`)
if res.Error == nil || res.Error.Code != tools.CodeDenied {
t.Fatalf("an unlisted role must be denied, got %+v", res)
}
}
func TestActivityBreakdownRejectsAnUnknownPeriod(t *testing.T) {
h := testutil.New(t)
admin, _ := seeded(t, h)
res := runBreakdown(t, h, admin, `{"period":"since-tuesday"}`)
if res.Error == nil || res.Error.Code != tools.CodeInvalidInput {
t.Fatalf("want invalid input for an unknown period, got %+v", res)
}
}
func TestActivityBreakdownCountsKindsBeforeLimiting(t *testing.T) {
// "How many kinds are there" must stay true even when the list is shorter,
// and the shortening must be declared rather than silent.
h := testutil.New(t)
admin, _ := seeded(t, h)
got := data(t, runBreakdown(t, h, admin, `{"limit":1}`))
if n := got["distinctKinds"].(float64); n != 2 {
t.Errorf("distinctKinds = %v, want 2 — counted before the limit", n)
}
if n, ok := got["omittedKinds"].(float64); !ok || n != 1 {
t.Errorf("omittedKinds = %v, want 1 — a shortened list must say so", got["omittedKinds"])
}
}
func TestActivityBreakdownReportsEmptyAsAnAnswer(t *testing.T) {
h := testutil.New(t)
empty := freshOrg(t, h, "empty-co")
admin := authctx.Identity{UserID: "u", OrgID: empty, Role: "admin", Email: "boss@example.test"}
got := data(t, runBreakdown(t, h, admin, `{}`))
if n := got["totalEvents"].(float64); n != 0 {
t.Fatalf("totalEvents = %v, want 0 on an empty log", n)
}
if _, ok := got["note"]; !ok {
t.Error("an empty result must say it is a real answer, not a failure to look")
}
}
func contains(haystack, needle string) bool {
return len(haystack) >= len(needle) && (func() bool {
for i := 0; i+len(needle) <= len(haystack); i++ {
if haystack[i:i+len(needle)] == needle {
return true
}
}
return false
})()
}