Add Discord event announcement functionality to backend
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit introduces a new Discord event announcer to the backend, allowing for event announcements via Discord. It includes the addition of new environment variables for Discord configuration in `.env.example` and `compose.yaml`. The `main.go` file has been updated to initialize the announcer, and a new `discord` package has been created, containing the announcer logic and tests. Additionally, the service layer has been modified to support bracket draft management, enhancing the overall event workflow. Integration tests have been updated to ensure proper functionality of the new features.
This commit is contained in:
2026-07-19 11:21:11 +03:00
parent b7a78b4384
commit ae19c03542
31 changed files with 1632 additions and 88 deletions

View File

@@ -0,0 +1,112 @@
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
err error
hasDeadline bool
}
func (a *recordingAnnouncer) AnnounceEventCreated(ctx context.Context, event domain.Event) error {
a.events = append(a.events, event)
_, 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))
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(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))
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))
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 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),
}
}