211 lines
7.0 KiB
Go
211 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|
|
|
|
type communitySettingsStore struct {
|
|
Store
|
|
saved domain.CommunitySettings
|
|
audits int
|
|
}
|
|
|
|
func (s *communitySettingsStore) UpdateCommunitySettings(_ context.Context, settings domain.CommunitySettings) (domain.CommunitySettings, error) {
|
|
s.saved = settings
|
|
return settings, nil
|
|
}
|
|
|
|
func (s *communitySettingsStore) AppendAudit(_ context.Context, _, _, _ string, _ any) error {
|
|
s.audits++
|
|
return nil
|
|
}
|
|
|
|
func TestUpdateCommunitySettingsRequiresAdminAndAudits(t *testing.T) {
|
|
store := &communitySettingsStore{}
|
|
service := New(store, &recordingPublisher{}, nil)
|
|
now := time.Date(2026, time.July, 19, 12, 0, 0, 0, time.UTC)
|
|
service.Now = func() time.Time { return now }
|
|
|
|
if _, err := service.UpdateCommunitySettings(context.Background(), domain.Account{Role: domain.RoleModerator}, ""); !errors.Is(err, domain.ErrForbidden) {
|
|
t.Fatalf("moderator must be forbidden, got %v", err)
|
|
}
|
|
out, err := service.UpdateCommunitySettings(context.Background(), domain.Account{ID: "admin", Role: domain.RoleAdmin}, " https://discord.gg/mixmaker ")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.DiscordInviteURL != "https://discord.gg/mixmaker" || out.UpdatedBy != "admin" || !out.UpdatedAt.Equal(now) {
|
|
t.Fatalf("unexpected settings: %+v", out)
|
|
}
|
|
if store.audits != 1 {
|
|
t.Fatalf("expected one audit entry, got %d", store.audits)
|
|
}
|
|
}
|
|
|
|
type playerProfileStore struct {
|
|
Store
|
|
players []domain.Player
|
|
saved domain.Player
|
|
err error
|
|
}
|
|
|
|
func (s *playerProfileStore) ListPlayers(context.Context) ([]domain.Player, error) {
|
|
return s.players, nil
|
|
}
|
|
|
|
func (s *playerProfileStore) UpdatePlayer(_ context.Context, player domain.Player) (domain.Player, error) {
|
|
s.saved = player
|
|
return player, s.err
|
|
}
|
|
|
|
func TestUpdateOwnProfileTrimsIdentityAndClearsBattleTag(t *testing.T) {
|
|
store := &playerProfileStore{players: []domain.Player{{ID: "player"}}}
|
|
service := New(store, &recordingPublisher{}, nil)
|
|
name, battleTag := " New Name ", ""
|
|
out, err := service.UpdateOwnProfile(context.Background(),
|
|
domain.Account{ID: "account"},
|
|
domain.Player{ID: "player", AccountID: "account", DisplayName: "Old", BattleTag: "Old#1234"},
|
|
UpdateOwnProfileInput{
|
|
DisplayName: &name, BattleTag: &battleTag,
|
|
Ratings: domain.Ratings{Tank: 1, Damage: 20, Support: 40},
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.DisplayName != "New Name" || out.BattleTag != "" {
|
|
t.Fatalf("identity was not normalized and cleared: %+v", out)
|
|
}
|
|
if _, err = service.UpdateOwnProfile(context.Background(),
|
|
domain.Account{ID: "other"}, domain.Player{AccountID: "account"},
|
|
UpdateOwnProfileInput{},
|
|
); !errors.Is(err, domain.ErrForbidden) {
|
|
t.Fatalf("profile updates must remain self-only, got %v", err)
|
|
}
|
|
}
|
|
|
|
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),
|
|
}
|
|
}
|