package application import ( "context" "errors" "testing" "time" "mixmaker/backend/internal/domain" ) type createEventStore struct { Store created domain.Event err error } func (s *createEventStore) CreateEvent(_ context.Context, event domain.Event) (domain.Event, error) { s.created = event if s.err != nil { return domain.Event{}, s.err } return event, nil } type recordingPublisher struct { topics []string } func (p *recordingPublisher) Publish(topic string, _ any) { p.topics = append(p.topics, topic) } type recordingAnnouncer struct { events []domain.Event pings []bool err error hasDeadline bool } func (a *recordingAnnouncer) AnnounceEventCreated(ctx context.Context, event domain.Event, pingEveryone bool) error { a.events = append(a.events, event) a.pings = append(a.pings, pingEveryone) _, a.hasDeadline = ctx.Deadline() return a.err } func TestCreateEventAnnouncesAfterPersistence(t *testing.T) { store := &createEventStore{} bus := &recordingPublisher{} announcer := &recordingAnnouncer{} service := New(store, bus, announcer) now := time.Date(2026, time.July, 19, 8, 0, 0, 0, time.UTC) service.Now = func() time.Time { return now } created, err := service.CreateEvent(context.Background(), domain.Account{ID: "admin", Role: domain.RoleAdmin}, validEvent(now), CreateEventOptions{PingEveryone: true}) if err != nil { t.Fatal(err) } if len(announcer.events) != 1 { t.Fatalf("expected one announcement, got %d", len(announcer.events)) } if announcer.events[0].ID != created.ID || created.ID == "" { t.Fatalf("announced event ID %q does not match created event ID %q", announcer.events[0].ID, created.ID) } if !announcer.hasDeadline { t.Fatal("announcement context must have a timeout") } if len(announcer.pings) != 1 || !announcer.pings[0] { t.Fatalf("expected @everyone option, got %v", announcer.pings) } if len(bus.topics) != 1 || bus.topics[0] != "events" { t.Fatalf("unexpected published topics: %v", bus.topics) } } func TestCreateEventDoesNotAnnouncePersistenceFailure(t *testing.T) { persistenceErr := errors.New("database unavailable") store := &createEventStore{err: persistenceErr} announcer := &recordingAnnouncer{} service := New(store, &recordingPublisher{}, announcer) now := time.Date(2026, time.July, 19, 8, 0, 0, 0, time.UTC) service.Now = func() time.Time { return now } _, err := service.CreateEvent(context.Background(), domain.Account{ID: "admin", Role: domain.RoleAdmin}, validEvent(now), CreateEventOptions{PingEveryone: true}) if !errors.Is(err, persistenceErr) { t.Fatalf("expected persistence error, got %v", err) } if len(announcer.events) != 0 { t.Fatalf("expected no announcements, got %d", len(announcer.events)) } } func TestCreateEventIgnoresAnnouncementFailure(t *testing.T) { announcementErr := errors.New("discord unavailable") announcer := &recordingAnnouncer{err: announcementErr} service := New(&createEventStore{}, &recordingPublisher{}, announcer) now := time.Date(2026, time.July, 19, 8, 0, 0, 0, time.UTC) service.Now = func() time.Time { return now } created, err := service.CreateEvent(context.Background(), domain.Account{ID: "admin", Role: domain.RoleAdmin}, validEvent(now), CreateEventOptions{PingEveryone: true}) if err != nil { t.Fatalf("announcement failure must not fail event creation: %v", err) } if created.ID == "" || len(announcer.events) != 1 { t.Fatalf("event was not created and announced exactly once: event=%+v announcements=%d", created, len(announcer.events)) } } func TestCreateEventCanAnnounceWithoutEveryonePing(t *testing.T) { announcer := &recordingAnnouncer{} service := New(&createEventStore{}, &recordingPublisher{}, announcer) now := time.Date(2026, time.July, 19, 8, 0, 0, 0, time.UTC) service.Now = func() time.Time { return now } if _, err := service.CreateEvent(context.Background(), domain.Account{ID: "admin", Role: domain.RoleAdmin}, validEvent(now), CreateEventOptions{}); err != nil { t.Fatal(err) } if len(announcer.events) != 1 || len(announcer.pings) != 1 || announcer.pings[0] { t.Fatalf("expected one announcement without @everyone, events=%d pings=%v", len(announcer.events), announcer.pings) } } func validEvent(now time.Time) domain.Event { return domain.Event{ Name: "Sunday Mix", Description: "Community scrim", StartsAt: now.Add(24 * time.Hour), EndsAt: now.Add(28 * time.Hour), } }