This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/notifications/sources_test.go
Jeffrey Smith f0dd43144e rebrand: Switchboard Core → Armature
- Rename Go module switchboard-core → armature (155+ files)
- Rename Docker image → gobha/armature
- Rename K8s resources, secrets, deployments
- Rename Prometheus metrics switchboard_* → armature_*
- Rename env vars SWITCHBOARD_ADMIN_* → ARMATURE_ADMIN_*
- Rename DB names switchboard_core* → armature*
- Update all frontend branding, notification templates, docs
- Update CI scripts, e2e tests, Keycloak realm, nginx conf
- Rename scripts/switchboard-ca.sh → scripts/armature-ca.sh
- Rename k8s/switchboard.yaml → k8s/armature.yaml
- Rename chart alerting/dashboard files
- Fix: DockerHub push uses env: binding for secret injection
- Helm chart updated (name, labels, template functions, dashboard, alerting)
- Replace favicon/icon assets with Armature brand

No functional changes. Pure mechanical rename + CI fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 21:39:58 +00:00

247 lines
7.0 KiB
Go

package notifications
import (
"context"
"testing"
"time"
"armature/models"
)
// mockNotifStore implements store.NotificationStore for testing.
type mockNotifStore struct {
items []models.Notification
}
func (m *mockNotifStore) Create(_ context.Context, n *models.Notification) error {
n.ID = "mock-id"
n.CreatedAt = time.Now()
m.items = append(m.items, *n)
return nil
}
func (m *mockNotifStore) ListByUser(_ context.Context, _ string, _, _ int, _ bool) ([]models.Notification, int, error) {
return m.items, len(m.items), nil
}
func (m *mockNotifStore) MarkRead(_ context.Context, _, _ string) error { return nil }
func (m *mockNotifStore) MarkAllRead(_ context.Context, _ string) error { return nil }
func (m *mockNotifStore) Delete(_ context.Context, _, _ string) error { return nil }
func (m *mockNotifStore) UnreadCount(_ context.Context, _ string) (int, error) {
return len(m.items), nil
}
func (m *mockNotifStore) DeleteOlderThan(_ context.Context, _ time.Time) (int64, error) {
return 0, nil
}
// newTestService creates a Service with a mock store for testing sources.
func newTestService() (*Service, *mockNotifStore) {
ms := &mockNotifStore{}
svc := NewService(ms, nil) // no hub
return svc, ms
}
// ── NotifyKBReady ─────────────────────────
func TestNotifyKBReady(t *testing.T) {
svc, ms := newTestService()
NotifyKBReady(svc, "user1", "kb1", "My KB", 42)
if len(ms.items) != 1 {
t.Fatalf("expected 1 notification, got %d", len(ms.items))
}
n := ms.items[0]
if n.UserID != "user1" {
t.Errorf("user_id: got %q, want %q", n.UserID, "user1")
}
if n.Type != models.NotifTypeKBReady {
t.Errorf("type: got %q, want %q", n.Type, models.NotifTypeKBReady)
}
if n.ResourceType != models.ResourceTypeKnowledgeBase {
t.Errorf("resource_type: got %q, want %q", n.ResourceType, models.ResourceTypeKnowledgeBase)
}
if n.ResourceID != "kb1" {
t.Errorf("resource_id: got %q, want %q", n.ResourceID, "kb1")
}
}
func TestNotifyKBReady_NilService(t *testing.T) {
// Should not panic
NotifyKBReady(nil, "user1", "kb1", "My KB", 10)
}
// ── NotifyKBError ─────────────────────────
func TestNotifyKBError(t *testing.T) {
svc, ms := newTestService()
NotifyKBError(svc, "user1", "kb2", "Bad KB", "chunk failed")
if len(ms.items) != 1 {
t.Fatalf("expected 1 notification, got %d", len(ms.items))
}
n := ms.items[0]
if n.Type != models.NotifTypeKBError {
t.Errorf("type: got %q, want %q", n.Type, models.NotifTypeKBError)
}
if n.Body != "chunk failed" {
t.Errorf("body: got %q, want %q", n.Body, "chunk failed")
}
}
// ── NotifyGroupMemberAdded ────────────────
func TestNotifyGroupMemberAdded(t *testing.T) {
svc, ms := newTestService()
NotifyGroupMemberAdded(svc, "user1", "grp1", "Engineering")
if len(ms.items) != 1 {
t.Fatalf("expected 1 notification, got %d", len(ms.items))
}
n := ms.items[0]
if n.Type != models.NotifTypeGrantChanged {
t.Errorf("type: got %q, want %q", n.Type, models.NotifTypeGrantChanged)
}
if n.ResourceType != models.ResourceTypeGroup {
t.Errorf("resource_type: got %q, want %q", n.ResourceType, models.ResourceTypeGroup)
}
if n.ResourceID != "grp1" {
t.Errorf("resource_id: got %q, want %q", n.ResourceID, "grp1")
}
}
// ── NotifyGroupMemberRemoved ──────────────
func TestNotifyGroupMemberRemoved(t *testing.T) {
svc, ms := newTestService()
NotifyGroupMemberRemoved(svc, "user1", "grp1", "Engineering")
if len(ms.items) != 1 {
t.Fatalf("expected 1 notification, got %d", len(ms.items))
}
n := ms.items[0]
if n.Type != models.NotifTypeGrantChanged {
t.Errorf("type: got %q", n.Type)
}
}
// ── NotifyUserMentioned ───────────────────
func TestNotifyUserMentioned(t *testing.T) {
svc, ms := newTestService()
NotifyUserMentioned(svc, "mentioned-user", "chan1", "from-user", "hey @you check this")
if len(ms.items) != 1 {
t.Fatalf("expected 1 notification, got %d", len(ms.items))
}
n := ms.items[0]
if n.UserID != "mentioned-user" {
t.Errorf("user_id: got %q", n.UserID)
}
if n.Type != models.NotifTypeUserMentioned {
t.Errorf("type: got %q, want %q", n.Type, models.NotifTypeUserMentioned)
}
if n.Body != "hey @you check this" {
t.Errorf("body: got %q", n.Body)
}
if n.ResourceType != models.ResourceTypeChannel {
t.Errorf("resource_type: got %q", n.ResourceType)
}
if n.ResourceID != "chan1" {
t.Errorf("resource_id: got %q", n.ResourceID)
}
}
func TestNotifyUserMentioned_NilService(t *testing.T) {
NotifyUserMentioned(nil, "u1", "c1", "u2", "hello") // should not panic
}
// ── NotifyWorkflowClaimed ─────────────────
func TestNotifyWorkflowClaimed(t *testing.T) {
svc, ms := newTestService()
NotifyWorkflowClaimed(svc, "claimer1", "assign1", "chan1")
if len(ms.items) != 1 {
t.Fatalf("expected 1 notification, got %d", len(ms.items))
}
n := ms.items[0]
if n.UserID != "claimer1" {
t.Errorf("user_id: got %q", n.UserID)
}
if n.Type != models.NotifTypeWorkflowClaimed {
t.Errorf("type: got %q, want %q", n.Type, models.NotifTypeWorkflowClaimed)
}
if n.ResourceType != models.ResourceTypeChannel {
t.Errorf("resource_type: got %q", n.ResourceType)
}
if n.ResourceID != "chan1" {
t.Errorf("resource_id: got %q", n.ResourceID)
}
}
func TestNotifyWorkflowClaimed_NilService(t *testing.T) {
NotifyWorkflowClaimed(nil, "u1", "a1", "c1") // should not panic
}
// ── NotifyMany ────────────────────────────
func TestNotifyMany(t *testing.T) {
svc, ms := newTestService()
template := models.Notification{
Type: "test.broadcast",
Title: "Hello everyone",
}
svc.NotifyMany(context.Background(), []string{"u1", "u2", "u3"}, template)
if len(ms.items) != 3 {
t.Fatalf("expected 3 notifications, got %d", len(ms.items))
}
seen := map[string]bool{}
for _, n := range ms.items {
seen[n.UserID] = true
if n.Type != "test.broadcast" {
t.Errorf("type: got %q", n.Type)
}
}
for _, uid := range []string{"u1", "u2", "u3"} {
if !seen[uid] {
t.Errorf("missing notification for %s", uid)
}
}
}
// ── Email subject lines for new types ─────
func TestSubjectForNotification_NewTypes(t *testing.T) {
tests := []struct {
notifType string
title string
wantPfx string
}{
{"memory.extracted", "3 new memories", "[Test] "},
{"user.mentioned", "You were mentioned", "[Test] "},
{"workflow.claimed", "Assignment claimed", "[Test] "},
{"task.completed", "Task done", "[Test] "},
{"task.budget_exceeded", "Budget hit", "[Test] "},
}
for _, tt := range tests {
n := &models.Notification{Type: tt.notifType, Title: tt.title}
got := SubjectForNotification(n, "Test")
if got == "" {
t.Errorf("SubjectForNotification(%q) returned empty", tt.notifType)
}
// All should at least have the instance prefix
if len(got) < len(tt.wantPfx) {
t.Errorf("SubjectForNotification(%q) too short: %q", tt.notifType, got)
}
}
}