package events_test import ( "context" "testing" "atlas9.dev/c/core" "atlas9.dev/c/core/assert" "atlas9.dev/c/core/iam" "atlas9.dev/c/demo/events" "atlas9.dev/c/demo/lib/slack" "atlas9.dev/c/demo/lib/todos" "atlas9.dev/c/demo/lib/webhooks" ) // The fakes below embed their interface so the unused methods exist (and panic // if the dispatcher ever calls them); each overrides only what Emit exercises. type fakeAudit struct { iam.AuditStore entries []iam.AuditEntry } func (f *fakeAudit) Append(ctx context.Context, e *iam.AuditEntry) error { f.entries = append(f.entries, *e) return nil } type fakeWebhookStore struct { webhooks.Store eps []webhooks.Endpoint } func (f fakeWebhookStore) ListActiveForEvent(ctx context.Context, tenant core.ID, eventType string) ([]webhooks.Endpoint, error) { return f.eps, nil } type fakeSlackStore struct { slack.Store eps []slack.Endpoint } func (f fakeSlackStore) ListActiveForEvent(ctx context.Context, tenant core.ID, eventType string) ([]slack.Endpoint, error) { return f.eps, nil } type webhookQueue struct{ n int } func (q *webhookQueue) Create(ctx context.Context, d webhooks.Delivery) error { q.n++; return nil } type slackQueue struct{ n int } func (q *slackQueue) Create(ctx context.Context, d slack.Delivery) error { q.n++; return nil } func dispatcher(audit *fakeAudit, ws fakeWebhookStore, wq *webhookQueue, ss fakeSlackStore, sq *slackQueue) events.Dispatcher { return events.Dispatcher{ Audit: audit, Webhooks: webhooks.Fanout{Endpoints: ws, Deliveries: wq}, Slack: slack.Fanout{Endpoints: ss, Deliveries: sq}, } } // Audit is the floor: it is written even when no channel has a subscriber, so a // mutation always leaves an audit record regardless of notification config. func TestDispatcher_Emit_AlwaysAudits(t *testing.T) { audit := &fakeAudit{} wq, sq := &webhookQueue{}, &slackQueue{} d := dispatcher(audit, fakeWebhookStore{}, wq, fakeSlackStore{}, sq) item := todos.Item{ID: core.NewID("itm"), Tenant: core.NewID("t"), Title: "Buy milk"} err := d.Emit(context.Background(), item.Tenant, todos.ItemCreated{Item: item}) assert.Ok(t, err) assert.Eq(t, len(audit.entries), 1) assert.Eq(t, audit.entries[0].Action, "Todos_CreateItem") assert.Eq(t, audit.entries[0].Resource, item.ID.String()) assert.Eq(t, audit.entries[0].Tenant, item.Tenant) // No subscribers, so nothing was enqueued on either channel. assert.Eq(t, wq.n, 0) assert.Eq(t, sq.n, 0) } // A known event with subscribers fans out to every channel, and still audits. func TestDispatcher_Emit_FansOutToChannels(t *testing.T) { tenant := core.NewID("t") audit := &fakeAudit{} wq, sq := &webhookQueue{}, &slackQueue{} d := dispatcher( audit, fakeWebhookStore{eps: []webhooks.Endpoint{{ID: core.NewID("whk")}}}, wq, fakeSlackStore{eps: []slack.Endpoint{{ID: core.NewID("slk")}}}, sq, ) item := todos.Item{ID: core.NewID("itm"), Tenant: tenant, Title: "x"} err := d.Emit(context.Background(), tenant, todos.ItemUpdated{Item: item}) assert.Ok(t, err) assert.Eq(t, wq.n, 1) assert.Eq(t, sq.n, 1) assert.Eq(t, len(audit.entries), 1) assert.Eq(t, audit.entries[0].Action, "Todos_UpdateItem") } // An event with no dispatch case is a programming error: Emit reports it and // writes nothing, rather than silently dropping the mutation's audit record. func TestDispatcher_Emit_UnknownEvent(t *testing.T) { audit := &fakeAudit{} wq, sq := &webhookQueue{}, &slackQueue{} d := dispatcher(audit, fakeWebhookStore{}, wq, fakeSlackStore{}, sq) type bogus struct{} err := d.Emit(context.Background(), core.NewID("t"), bogus{}) assert.Eq(t, err != nil, true) assert.Eq(t, len(audit.entries), 0) }