package store_test import ( "context" "database/sql" "encoding/json" "errors" "testing" "atlas9.dev/c/core" "atlas9.dev/c/core/assert" "atlas9.dev/c/core/dbi" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/lib/access" "atlas9.dev/c/demo/lib/slack" "atlas9.dev/c/demo/lib/todos" "atlas9.dev/c/demo/store" ) // seedSlackEndpoint inserts a Slack endpoint directly, bypassing the store's // sealed-URL guard so read-path tests don't need a real encryptor. func seedSlackEndpoint(t *testing.T, db *sql.DB, e slack.Endpoint) { t.Helper() types, err := json.Marshal(e.EventTypes) assert.Ok(t, err) _, err = db.ExecContext(context.Background(), ` INSERT INTO slack_endpoints (id, tenant, name, event_types, active, dek_id, url_enc) VALUES ($1, $2, $3, $4, $5, $6, $7) `, e.ID, e.Tenant, e.Name, string(types), e.Active, core.NewID("dek"), []byte("dummy")) assert.Ok(t, err) } func slackStore(tx dbi.DBI) slack.Store { return store.NewSqliteSlackStore(tx, access.AllowAll{}) } func TestSlackStore_GetEndpoint_RoundTrip(t *testing.T) { db := setupTestDB(t) tenant := seedTenant(t, db) ctx := context.Background() ep := slack.Endpoint{ ID: core.NewID("slk"), Tenant: tenant, Name: "#general", EventTypes: []string{todos.EventItemCreated, todos.EventItemUpdated}, Active: true, } seedSlackEndpoint(t, db, ep) var got slack.Endpoint assert.Ok(t, dbi.ReadOnly(ctx, db, func(tx dbi.DBI) error { return slackStore(tx).GetEndpoint(ctx, tenant, ep.ID, &got) })) assert.Eq(t, got.Name, "#general") // EventTypes survives the JSON column round-trip. assert.Eq(t, len(got.EventTypes), 2) assert.Eq(t, got.EventTypes[0], todos.EventItemCreated) } func TestSlackStore_ListActiveForEvent(t *testing.T) { db := setupTestDB(t) tenant := seedTenant(t, db) other := seedTenant(t, db) ctx := context.Background() // Subscribed and active: should match. match := slack.Endpoint{ID: core.NewID("slk"), Tenant: tenant, Name: "a", EventTypes: []string{todos.EventItemCreated}, Active: true} // Subscribed to the type but inactive: excluded. inactive := slack.Endpoint{ID: core.NewID("slk"), Tenant: tenant, Name: "b", EventTypes: []string{todos.EventItemCreated}, Active: false} // Active but subscribed to a different type: excluded. otherType := slack.Endpoint{ID: core.NewID("slk"), Tenant: tenant, Name: "c", EventTypes: []string{todos.EventItemUpdated}, Active: true} // Right type and active, but a different tenant: excluded. otherTenant := slack.Endpoint{ID: core.NewID("slk"), Tenant: other, Name: "d", EventTypes: []string{todos.EventItemCreated}, Active: true} seedSlackEndpoint(t, db, match) seedSlackEndpoint(t, db, inactive) seedSlackEndpoint(t, db, otherType) seedSlackEndpoint(t, db, otherTenant) var found []slack.Endpoint assert.Ok(t, dbi.ReadOnly(ctx, db, func(tx dbi.DBI) error { var err error found, err = slackStore(tx).ListActiveForEvent(ctx, tenant, todos.EventItemCreated) return err })) assert.Eq(t, len(found), 1) assert.Eq(t, found[0].ID, match.ID) } // A plaintext (unsealed) webhook URL must never reach the column. func TestSlackStore_CreateEndpoint_RejectsUnsealedURL(t *testing.T) { db := setupTestDB(t) tenant := seedTenant(t, db) ctx := context.Background() ep := slack.Endpoint{ ID: core.NewID("slk"), Tenant: tenant, Name: "a", Active: true, DekID: core.NewID("dek"), URLEnc: []byte("not-sealed"), } err := dbi.ReadWrite(ctx, db, func(tx dbi.DBI) error { return slackStore(tx).CreateEndpoint(ctx, &ep) }) assert.Eq(t, err != nil, true) } // The guard runs before any write: a caller without the cap is refused. func TestSlackStore_CreateEndpoint_Denied(t *testing.T) { db := setupTestDB(t) tenant := seedTenant(t, db) ctx := context.Background() ep := slack.Endpoint{ID: core.NewID("slk"), Tenant: tenant, Name: "a", Active: true} err := dbi.ReadWrite(ctx, db, func(tx dbi.DBI) error { // A real guard with an empty context grants nothing. return store.NewSqliteSlackStore(tx, access.ContextGuard{}).CreateEndpoint(ctx, &ep) }) assert.Eq(t, errors.Is(err, iam.ErrForbidden), true) }