This commit introduces functionality for managing event-specific RSVP roles in Discord, including roles for "Registered", "Going", "Maybe", and "Not Going". The `RoleWorker` has been enhanced to handle these roles based on user registrations, with localization support for role names in Russian and English. Additionally, the database schema has been updated to accommodate new role types and actions, and the service layer has been modified to trigger role synchronization upon relevant event updates. Tests have been added to ensure the correct behavior of the new RSVP handling logic.
509 lines
19 KiB
Go
509 lines
19 KiB
Go
package discord
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"mixmaker/backend/internal/application"
|
|
"mixmaker/backend/internal/domain"
|
|
)
|
|
|
|
type roleStoreFake struct {
|
|
mu sync.Mutex
|
|
jobs []application.DiscordRoleSyncJob
|
|
roles map[string]application.DiscordManagedRole
|
|
assignments map[string]map[string]bool
|
|
rosters map[string]application.DiscordRoleRoster
|
|
registrations map[string]application.DiscordRoleRegistration
|
|
active []application.DiscordRoleRoster
|
|
warnings []string
|
|
}
|
|
|
|
func newRoleStoreFake() *roleStoreFake {
|
|
return &roleStoreFake{
|
|
roles: make(map[string]application.DiscordManagedRole),
|
|
assignments: make(map[string]map[string]bool),
|
|
rosters: make(map[string]application.DiscordRoleRoster),
|
|
registrations: make(map[string]application.DiscordRoleRegistration),
|
|
}
|
|
}
|
|
|
|
func (s *roleStoreFake) SeedDiscordRoleSyncJobs(context.Context) error { return nil }
|
|
|
|
func (s *roleStoreFake) ClaimDiscordRoleSyncJob(context.Context) (application.DiscordRoleSyncJob, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if len(s.jobs) == 0 {
|
|
return application.DiscordRoleSyncJob{}, domain.ErrNotFound
|
|
}
|
|
job := s.jobs[0]
|
|
s.jobs = s.jobs[1:]
|
|
job.Attempts++
|
|
return job, nil
|
|
}
|
|
|
|
func (s *roleStoreFake) CompleteDiscordRoleSyncJob(_ context.Context, _ int64, warning string) error {
|
|
s.warnings = append(s.warnings, warning)
|
|
return nil
|
|
}
|
|
|
|
func (s *roleStoreFake) RetryDiscordRoleSyncJob(_ context.Context, _ int64, message string, _ time.Time) error {
|
|
s.warnings = append(s.warnings, message)
|
|
return nil
|
|
}
|
|
|
|
func (s *roleStoreFake) ListDiscordManagedRoles(_ context.Context, eventID string) ([]application.DiscordManagedRole, error) {
|
|
out := make([]application.DiscordManagedRole, 0)
|
|
for _, role := range s.roles {
|
|
if role.Scope == application.DiscordRoleScopeGlobal || role.EventID == eventID {
|
|
out = append(out, role)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *roleStoreFake) UpsertDiscordManagedRole(_ context.Context, role application.DiscordManagedRole) error {
|
|
s.roles[managedRoleKey(role)] = role
|
|
return nil
|
|
}
|
|
|
|
func (s *roleStoreFake) DeleteDiscordManagedRole(_ context.Context, role application.DiscordManagedRole) error {
|
|
delete(s.assignments, role.DiscordRoleID)
|
|
delete(s.roles, managedRoleKey(role))
|
|
return nil
|
|
}
|
|
|
|
func (s *roleStoreFake) ListDiscordRoleAssignments(_ context.Context, roleID string) ([]string, error) {
|
|
out := make([]string, 0)
|
|
for userID := range s.assignments[roleID] {
|
|
out = append(out, userID)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *roleStoreFake) UpsertDiscordRoleAssignment(_ context.Context, roleID, userID string) error {
|
|
if s.assignments[roleID] == nil {
|
|
s.assignments[roleID] = make(map[string]bool)
|
|
}
|
|
s.assignments[roleID][userID] = true
|
|
return nil
|
|
}
|
|
|
|
func (s *roleStoreFake) DeleteDiscordRoleAssignment(_ context.Context, roleID, userID string) error {
|
|
delete(s.assignments[roleID], userID)
|
|
return nil
|
|
}
|
|
|
|
func (s *roleStoreFake) GetDiscordRoleRoster(_ context.Context, eventID string) (application.DiscordRoleRoster, error) {
|
|
roster, ok := s.rosters[eventID]
|
|
if !ok {
|
|
return roster, domain.ErrNotFound
|
|
}
|
|
return roster, nil
|
|
}
|
|
|
|
func (s *roleStoreFake) ListActiveDiscordRoleRosters(context.Context) ([]application.DiscordRoleRoster, error) {
|
|
return s.active, nil
|
|
}
|
|
|
|
func (s *roleStoreFake) GetDiscordRoleRegistration(_ context.Context, eventID string) (application.DiscordRoleRegistration, error) {
|
|
registration, ok := s.registrations[eventID]
|
|
if !ok {
|
|
return registration, domain.ErrNotFound
|
|
}
|
|
return registration, nil
|
|
}
|
|
|
|
func TestRoleWorkerReconcilesIdempotentlyAndRenames(t *testing.T) {
|
|
var mu sync.Mutex
|
|
roleNames := make(map[string]string)
|
|
methods := make([]string, 0)
|
|
nextID := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
methods = append(methods, request.Method+" "+request.URL.Path)
|
|
if request.Method == http.MethodPost && strings.HasSuffix(request.URL.Path, "/roles") {
|
|
var payload map[string]any
|
|
_ = json.NewDecoder(request.Body).Decode(&payload)
|
|
nextID++
|
|
id := "role-" + string(rune('0'+nextID))
|
|
roleNames[id], _ = payload["name"].(string)
|
|
response.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(response).Encode(map[string]string{"id": id})
|
|
return
|
|
}
|
|
if request.Method == http.MethodPatch {
|
|
var payload map[string]string
|
|
_ = json.NewDecoder(request.Body).Decode(&payload)
|
|
parts := strings.Split(request.URL.Path, "/")
|
|
roleNames[parts[len(parts)-1]] = payload["name"]
|
|
}
|
|
response.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
|
|
store := newRoleStoreFake()
|
|
item := testDiscordRoster("Alpha")
|
|
store.rosters["event-1"] = item
|
|
store.active = []application.DiscordRoleRoster{item}
|
|
rsvpRole := application.DiscordManagedRole{
|
|
Scope: application.DiscordRoleScopeEvent, EventID: "event-1",
|
|
Kind: application.DiscordRoleKindRegistered, DiscordRoleID: "role-rsvp", RoleName: "Зарегистрирован: Mix",
|
|
}
|
|
store.roles[managedRoleKey(rsvpRole)] = rsvpRole
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 1, EventID: "event-1", Action: application.DiscordRoleActionReconcile}}
|
|
worker, err := NewRoleWorker(store, RoleSyncConfig{
|
|
BotToken: "token", GuildID: "guild", APIBaseURL: server.URL, HTTPClient: server.Client(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if processed, err := worker.ProcessNext(context.Background()); err != nil || !processed {
|
|
t.Fatalf("first reconcile: processed=%v err=%v", processed, err)
|
|
}
|
|
names := make([]string, 0, len(roleNames))
|
|
for _, name := range roleNames {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
expectedNames := []string{"Alpha", "Alpha Captain", "Damage", "Support", "Tank"}
|
|
if strings.Join(names, ",") != strings.Join(expectedNames, ",") {
|
|
t.Fatalf("unexpected role names: %v", names)
|
|
}
|
|
if got := assignmentCount(store.assignments); got != 11 {
|
|
t.Fatalf("expected 11 managed assignments, got %d", got)
|
|
}
|
|
if _, ok := store.roles[managedRoleKey(rsvpRole)]; !ok {
|
|
t.Fatal("roster reconcile deleted an RSVP role")
|
|
}
|
|
|
|
firstRequestCount := len(methods)
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 2, EventID: "event-1", Action: application.DiscordRoleActionReconcile}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(methods) != firstRequestCount {
|
|
t.Fatalf("idempotent reconcile made %d extra Discord calls", len(methods)-firstRequestCount)
|
|
}
|
|
|
|
renamed := testDiscordRoster("Night Owls")
|
|
store.rosters["event-1"] = renamed
|
|
store.active = []application.DiscordRoleRoster{renamed}
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 3, EventID: "event-1", Action: application.DiscordRoleActionReconcile}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
patches := 0
|
|
for _, method := range methods[firstRequestCount:] {
|
|
if strings.HasPrefix(method, http.MethodPatch+" ") {
|
|
patches++
|
|
}
|
|
}
|
|
if patches != 2 {
|
|
t.Fatalf("expected team and captain role rename, got %d PATCH calls", patches)
|
|
}
|
|
|
|
roleChanged := testDiscordRoster("Night Owls")
|
|
roleChanged.Roster.Teams[0].Slots[0].Role = domain.Damage
|
|
roleChanged.Roster.Teams[0].Slots[1].Role = domain.Tank
|
|
store.rosters["event-1"] = roleChanged
|
|
store.active = []application.DiscordRoleRoster{roleChanged}
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 4, EventID: "event-1", Action: application.DiscordRoleActionReconcile}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tankRoleID := managedRoleID(store.roles, application.DiscordRoleKindTank)
|
|
damageRoleID := managedRoleID(store.roles, application.DiscordRoleKindDamage)
|
|
if !store.assignments[tankRoleID]["user-2"] || store.assignments[tankRoleID]["user-1"] {
|
|
t.Fatalf("tank assignment did not follow changed slot: %v", store.assignments[tankRoleID])
|
|
}
|
|
if !store.assignments[damageRoleID]["user-1"] || store.assignments[damageRoleID]["user-2"] {
|
|
t.Fatalf("damage assignment did not follow changed slot: %v", store.assignments[damageRoleID])
|
|
}
|
|
}
|
|
|
|
func TestRoleWorkerReconcilesRSVPStatusAndEventRename(t *testing.T) {
|
|
nextID := 0
|
|
patches := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
if request.Method == http.MethodPost {
|
|
nextID++
|
|
_ = json.NewEncoder(response).Encode(map[string]string{"id": "rsvp-role-" + string(rune('0'+nextID))})
|
|
return
|
|
}
|
|
if request.Method == http.MethodPatch {
|
|
patches++
|
|
}
|
|
response.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
store := newRoleStoreFake()
|
|
store.registrations["event-1"] = testDiscordRegistration("Sunday Mix", domain.Going)
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 1, EventID: "event-1", Action: application.DiscordRoleActionRSVP}}
|
|
worker, err := NewRoleWorker(store, RoleSyncConfig{
|
|
BotToken: "token", GuildID: "guild", Locale: "ru", APIBaseURL: server.URL, HTTPClient: server.Client(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registeredID := eventRoleID(store.roles, "event-1", application.DiscordRoleKindRegistered)
|
|
goingID := eventRoleID(store.roles, "event-1", application.DiscordRoleKindGoing)
|
|
maybeID := eventRoleID(store.roles, "event-1", application.DiscordRoleKindMaybe)
|
|
notGoingID := eventRoleID(store.roles, "event-1", application.DiscordRoleKindNotGoing)
|
|
if !store.assignments[registeredID]["user-1"] || !store.assignments[goingID]["user-1"] {
|
|
t.Fatalf("initial Going roles were not assigned: %v", store.assignments)
|
|
}
|
|
|
|
store.registrations["event-1"] = testDiscordRegistration("Sunday Mix", domain.Maybe)
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 2, EventID: "event-1", Action: application.DiscordRoleActionRSVP}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if store.assignments[goingID]["user-1"] || !store.assignments[maybeID]["user-1"] || !store.assignments[registeredID]["user-1"] {
|
|
t.Fatalf("Going to Maybe transition was not reconciled: %v", store.assignments)
|
|
}
|
|
|
|
store.registrations["event-1"] = testDiscordRegistration("Sunday Mix", domain.NotGoing)
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 3, EventID: "event-1", Action: application.DiscordRoleActionRSVP}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if store.assignments[maybeID]["user-1"] || !store.assignments[notGoingID]["user-1"] || !store.assignments[registeredID]["user-1"] {
|
|
t.Fatalf("Maybe to NotGoing transition was not reconciled: %v", store.assignments)
|
|
}
|
|
|
|
renamed := testDiscordRegistration("Night Mix", domain.NotGoing)
|
|
store.registrations["event-1"] = renamed
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 4, EventID: "event-1", Action: application.DiscordRoleActionRSVP}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if patches != 4 {
|
|
t.Fatalf("expected four RSVP role renames, got %d", patches)
|
|
}
|
|
|
|
renamed.Registrations = nil
|
|
renamed.PlayerDiscordIDs = map[string]string{}
|
|
store.registrations["event-1"] = renamed
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 5, EventID: "event-1", Action: application.DiscordRoleActionRSVP}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, roleID := range []string{registeredID, goingID, maybeID, notGoingID} {
|
|
if len(store.assignments[roleID]) != 0 {
|
|
t.Fatalf("RSVP deletion left assignments on role %s: %v", roleID, store.assignments[roleID])
|
|
}
|
|
}
|
|
|
|
renamed.Registrations = []domain.RSVP{{EventID: "event-1", PlayerID: "guest", Status: domain.Going}}
|
|
store.registrations["event-1"] = renamed
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 6, EventID: "event-1", Action: application.DiscordRoleActionRSVP}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if warning := store.warnings[len(store.warnings)-1]; !strings.Contains(warning, "has no linked Discord account") {
|
|
t.Fatalf("guest warning was not stored: %q", warning)
|
|
}
|
|
}
|
|
|
|
func TestRoleWorkerStoresMissingGuildMemberWarning(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
if request.Method == http.MethodPost {
|
|
_ = json.NewEncoder(response).Encode(map[string]string{"id": strings.ReplaceAll(request.URL.Path, "/", "-") + time.Now().String()})
|
|
return
|
|
}
|
|
if request.Method == http.MethodPut && strings.Contains(request.URL.Path, "/members/missing/") {
|
|
http.Error(response, `{"message":"Unknown Member"}`, http.StatusNotFound)
|
|
return
|
|
}
|
|
response.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
store := newRoleStoreFake()
|
|
item := testDiscordRoster("Alpha")
|
|
item.PlayerDiscordIDs["player-1"] = "missing"
|
|
store.rosters["event-1"] = item
|
|
store.active = []application.DiscordRoleRoster{item}
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 1, EventID: "event-1", Action: application.DiscordRoleActionReconcile}}
|
|
worker, err := NewRoleWorker(store, RoleSyncConfig{BotToken: "token", GuildID: "guild", APIBaseURL: server.URL, HTTPClient: server.Client()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(store.warnings) != 1 || !strings.Contains(store.warnings[0], "not in the guild") {
|
|
t.Fatalf("missing member warning was not stored: %v", store.warnings)
|
|
}
|
|
}
|
|
|
|
func TestRoleWorkerTeardownPreservesOtherEventGlobalAssignments(t *testing.T) {
|
|
nextID := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
|
|
if request.Method == http.MethodPost {
|
|
nextID++
|
|
_ = json.NewEncoder(response).Encode(map[string]string{"id": "role-" + string(rune('0'+nextID))})
|
|
return
|
|
}
|
|
response.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
store := newRoleStoreFake()
|
|
first := testDiscordRoster("Alpha")
|
|
second := testDiscordRoster("Bravo")
|
|
second.Roster.EventID = "event-2"
|
|
second.Roster.Teams[0].EventID = "event-2"
|
|
second.Roster.Teams[0].ID = "team-2"
|
|
second.PlayerDiscordIDs = make(map[string]string)
|
|
for index := range second.Roster.Teams[0].Slots {
|
|
playerID := "other-" + string(rune('1'+index))
|
|
second.Roster.Teams[0].Slots[index].PlayerID = playerID
|
|
second.PlayerDiscordIDs[playerID] = "other-user-" + string(rune('1'+index))
|
|
}
|
|
second.Roster.Teams[0].CaptainPlayerID = second.Roster.Teams[0].Slots[0].PlayerID
|
|
store.rosters["event-1"], store.rosters["event-2"] = first, second
|
|
store.active = []application.DiscordRoleRoster{first, second}
|
|
store.jobs = []application.DiscordRoleSyncJob{
|
|
{ID: 1, EventID: "event-1", Action: application.DiscordRoleActionReconcile},
|
|
{ID: 2, EventID: "event-2", Action: application.DiscordRoleActionReconcile},
|
|
}
|
|
worker, err := NewRoleWorker(store, RoleSyncConfig{BotToken: "token", GuildID: "guild", APIBaseURL: server.URL, HTTPClient: server.Client()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
store.active = []application.DiscordRoleRoster{second}
|
|
store.jobs = []application.DiscordRoleSyncJob{{ID: 3, EventID: "event-1", Action: application.DiscordRoleActionTeardown}}
|
|
if _, err = worker.ProcessNext(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
globalAssignments := 0
|
|
for _, role := range store.roles {
|
|
if role.Scope == application.DiscordRoleScopeEvent && role.EventID == "event-1" {
|
|
t.Fatalf("event-1 role was not deleted: %+v", role)
|
|
}
|
|
if role.Scope == application.DiscordRoleScopeGlobal {
|
|
globalAssignments += len(store.assignments[role.DiscordRoleID])
|
|
for userID := range store.assignments[role.DiscordRoleID] {
|
|
if !strings.HasPrefix(userID, "other-user-") {
|
|
t.Fatalf("stale global assignment remained after teardown: %s", userID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if globalAssignments != 5 {
|
|
t.Fatalf("expected five global assignments for the other event, got %d", globalAssignments)
|
|
}
|
|
}
|
|
|
|
func TestRoleManagerReturnsRateLimit(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
|
|
response.Header().Set("Retry-After", "2")
|
|
http.Error(response, `{"message":"rate limited"}`, http.StatusTooManyRequests)
|
|
}))
|
|
defer server.Close()
|
|
manager, err := newRoleManager(RoleSyncConfig{BotToken: "token", GuildID: "guild", APIBaseURL: server.URL, HTTPClient: server.Client()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = manager.AddMemberRole(context.Background(), "user", "role")
|
|
var httpErr *discordHTTPError
|
|
if !errors.As(err, &httpErr) || httpErr.StatusCode != http.StatusTooManyRequests || retryDelay(1, err) != 2*time.Second {
|
|
t.Fatalf("unexpected rate limit error: %#v", err)
|
|
}
|
|
}
|
|
|
|
func TestRoleManagerReturnsServerError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) {
|
|
http.Error(response, `{"message":"temporary failure"}`, http.StatusBadGateway)
|
|
}))
|
|
defer server.Close()
|
|
manager, err := newRoleManager(RoleSyncConfig{BotToken: "token", GuildID: "guild", APIBaseURL: server.URL, HTTPClient: server.Client()})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = manager.DeleteRole(context.Background(), "role")
|
|
var httpErr *discordHTTPError
|
|
if !errors.As(err, &httpErr) || httpErr.StatusCode != http.StatusBadGateway {
|
|
t.Fatalf("unexpected server error: %#v", err)
|
|
}
|
|
}
|
|
|
|
func testDiscordRoster(name string) application.DiscordRoleRoster {
|
|
slots := []domain.Slot{
|
|
{PlayerID: "player-1", Role: domain.Tank},
|
|
{PlayerID: "player-2", Role: domain.Damage},
|
|
{PlayerID: "player-3", Role: domain.Damage},
|
|
{PlayerID: "player-4", Role: domain.Support},
|
|
{PlayerID: "player-5", Role: domain.Support},
|
|
}
|
|
discordIDs := make(map[string]string)
|
|
for index := 1; index <= 5; index++ {
|
|
discordIDs["player-"+string(rune('0'+index))] = "user-" + string(rune('0'+index))
|
|
}
|
|
return application.DiscordRoleRoster{
|
|
Roster: domain.RosterDraft{
|
|
EventID: "event-1",
|
|
Teams: []domain.Team{{
|
|
ID: "team-1", EventID: "event-1", Name: name, CaptainPlayerID: "player-1", Slots: slots,
|
|
}},
|
|
Confirmed: true,
|
|
},
|
|
PlayerDiscordIDs: discordIDs,
|
|
}
|
|
}
|
|
|
|
func testDiscordRegistration(name string, status domain.RSVPStatus) application.DiscordRoleRegistration {
|
|
return application.DiscordRoleRegistration{
|
|
Event: domain.Event{ID: "event-1", Name: name, State: domain.RegistrationOpen},
|
|
Registrations: []domain.RSVP{{
|
|
EventID: "event-1", PlayerID: "player-1", Status: status,
|
|
}},
|
|
PlayerDiscordIDs: map[string]string{"player-1": "user-1"},
|
|
}
|
|
}
|
|
|
|
func assignmentCount(assignments map[string]map[string]bool) int {
|
|
total := 0
|
|
for _, users := range assignments {
|
|
total += len(users)
|
|
}
|
|
return total
|
|
}
|
|
|
|
func managedRoleID(roles map[string]application.DiscordManagedRole, kind string) string {
|
|
for _, role := range roles {
|
|
if role.Scope == application.DiscordRoleScopeGlobal && role.Kind == kind {
|
|
return role.DiscordRoleID
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func eventRoleID(roles map[string]application.DiscordManagedRole, eventID, kind string) string {
|
|
for _, role := range roles {
|
|
if role.Scope == application.DiscordRoleScopeEvent && role.EventID == eventID && role.Kind == kind {
|
|
return role.DiscordRoleID
|
|
}
|
|
}
|
|
return ""
|
|
}
|