//go:build integration // Runs against a real Postgres with the dashboard API's migrations applied // (the lobby limit is a trigger from migration 000020): // // TEST_DB=postgres://... go test -tags integration ./internal/db/repos/settingsrepo package settingsrepo import ( "context" "database/sql" "errors" "os" "sync" "testing" _ "github.com/jackc/pgx/v5/stdlib" ) func testRepo(t *testing.T) (*Repo, int64) { t.Helper() dsn := os.Getenv("TEST_DB") if dsn == "" { t.Skip("TEST_DB not set") } db, err := sql.Open("pgx", dsn) if err != nil { t.Fatal(err) } t.Cleanup(func() { db.Close() }) const guild = 4242 if _, err := db.Exec(`DELETE FROM meeting_lobbies WHERE guild_id IN ($1, $2)`, guild, guild+1); err != nil { t.Fatal(err) } return NewRepo(db), guild } func TestMeetingLobbies(t *testing.T) { r, g := testRepo(t) ctx := context.Background() if ids, err := r.ListMeetingLobbies(ctx, g); err != nil || len(ids) != 0 { t.Fatalf("a fresh server has no lobbies: %v %v", ids, err) } for ch := int64(1); ch <= MaxMeetingLobbies; ch++ { added, err := r.AddMeetingLobby(ctx, g, ch) if err != nil || !added { t.Fatalf("adding lobby %d: added=%v err=%v", ch, added, err) } } if _, err := r.AddMeetingLobby(ctx, g, 99); !errors.Is(err, ErrMeetingLobbyLimit) { t.Errorf("an 11th lobby must hit the limit, got %v", err) } if added, err := r.AddMeetingLobby(ctx, g, 3); err != nil || added { t.Errorf("adding an existing lobby is a no-op even at the limit: added=%v err=%v", added, err) } if added, err := r.AddMeetingLobby(ctx, g+1, 99); err != nil || !added { t.Errorf("the limit is per server: added=%v err=%v", added, err) } if ok, _ := r.IsMeetingLobby(ctx, g, 7); !ok { t.Error("7 is a lobby") } if ok, _ := r.IsMeetingLobby(ctx, g, 99); ok { t.Error("99 isn't a lobby of this server (it is one of the other server's)") } if removed, err := r.RemoveMeetingLobby(ctx, g, 7); err != nil || !removed { t.Errorf("removing 7: removed=%v err=%v", removed, err) } if removed, _ := r.RemoveMeetingLobby(ctx, g, 7); removed { t.Error("7 was already removed") } if ok, _ := r.IsMeetingLobby(ctx, g, 7); ok { t.Error("7 is no longer a lobby") } if added, err := r.AddMeetingLobby(ctx, g, 99); err != nil || !added { t.Errorf("removing one makes room: added=%v err=%v", added, err) } ids, err := r.ListMeetingLobbies(ctx, g) if err != nil || len(ids) != MaxMeetingLobbies || ids[len(ids)-1] != 99 { t.Errorf("oldest first, newest last: %v %v", ids, err) } } // Two people adding a lobby at the same moment must not both fit under the limit. func TestMeetingLobbiesConcurrent(t *testing.T) { r, g := testRepo(t) ctx := context.Background() for ch := int64(1); ch < MaxMeetingLobbies; ch++ { if _, err := r.AddMeetingLobby(ctx, g, ch); err != nil { t.Fatal(err) } } var wg sync.WaitGroup var mu sync.Mutex succeeded := 0 for i := 0; i < 8; i++ { wg.Add(1) go func(ch int64) { defer wg.Done() if added, err := r.AddMeetingLobby(ctx, g, ch); err == nil && added { mu.Lock() succeeded++ mu.Unlock() } }(int64(100 + i)) } wg.Wait() if succeeded != 1 { t.Errorf("exactly one of the concurrent adds fits, %d did", succeeded) } if ids, _ := r.ListMeetingLobbies(ctx, g); len(ids) != MaxMeetingLobbies { t.Errorf("have %d lobbies, want %d", len(ids), MaxMeetingLobbies) } } func TestGetBetaSettings(t *testing.T) { r, _ := testRepo(t) db := r.db ctx := context.Background() _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS beta_settings ( id BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (id), guild_id BIGINT, role_id BIGINT, updated_by BIGINT, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`) if err != nil { t.Fatal(err) } var wasG, wasR sql.NullInt64 hadRow := db.QueryRowContext(ctx, `SELECT guild_id, role_id FROM beta_settings WHERE id`).Scan(&wasG, &wasR) == nil t.Cleanup(func() { if hadRow { _, _ = db.ExecContext(ctx, `UPDATE beta_settings SET guild_id=$1, role_id=$2 WHERE id`, wasG, wasR) } else { _, _ = db.ExecContext(ctx, `DELETE FROM beta_settings`) } }) _, err = db.ExecContext(ctx, `DELETE FROM beta_settings`) if err != nil { t.Fatal(err) } g, role, err := r.GetBetaSettings(ctx) if err != nil { t.Fatal(err) } if g != "" { t.Fatalf("g = %q, want empty", g) } if role != "" { t.Fatalf("role = %q, want empty", role) } _, err = db.ExecContext(ctx, `INSERT INTO beta_settings (id, guild_id) VALUES (TRUE, 1170934850157547600)`) if err != nil { t.Fatal(err) } g, role, err = r.GetBetaSettings(ctx) if err != nil { t.Fatal(err) } if g != "1170934850157547600" { t.Fatalf("g = %q, want %q", g, "1170934850157547600") } if role != "" { t.Fatalf("role = %q, want empty", role) } _, err = db.ExecContext(ctx, `UPDATE beta_settings SET role_id = 42`) if err != nil { t.Fatal(err) } _, role, err = r.GetBetaSettings(ctx) if err != nil { t.Fatal(err) } if role != "42" { t.Fatalf("role = %q, want %q", role, "42") } } func TestListBetaGuilds(t *testing.T) { r, _ := testRepo(t) db := r.db ctx := context.Background() if _, err := db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS beta_guilds ( guild_id BIGINT PRIMARY KEY, enabled_by BIGINT NOT NULL, enabled_at TIMESTAMPTZ NOT NULL DEFAULT NOW())`); err != nil { t.Fatal(err) } const g = 8800000000000004242 _, _ = db.ExecContext(ctx, `DELETE FROM beta_guilds WHERE guild_id = $1`, g) t.Cleanup(func() { _, _ = db.ExecContext(ctx, `DELETE FROM beta_guilds WHERE guild_id = $1`, g) }) has := func() bool { ids, err := r.ListBetaGuilds(ctx) if err != nil { t.Fatal(err) } for _, id := range ids { if id == "8800000000000004242" { return true } } return false } if has() { t.Fatal("not listed before opting in") } if _, err := db.ExecContext(ctx, `INSERT INTO beta_guilds (guild_id, enabled_by) VALUES ($1, 1)`, g); err != nil { t.Fatal(err) } if !has() { t.Fatal("listed after opting in") } } func TestBetaRegistered(t *testing.T) { r, _ := testRepo(t) ctx := context.Background() if _, err := r.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS beta_registered (guild_id BIGINT PRIMARY KEY)`); err != nil { t.Fatal(err) } const g = "8800000000000005151" _ = r.SetBetaRegistered(ctx, g, false) t.Cleanup(func() { _ = r.SetBetaRegistered(ctx, g, false) }) has := func() bool { ids, err := r.ListBetaRegistered(ctx) if err != nil { t.Fatal(err) } for _, id := range ids { if id == g { return true } } return false } if has() { t.Fatal("not remembered at first") } for i := 0; i < 2; i++ { // twice: harmless if err := r.SetBetaRegistered(ctx, g, true); err != nil { t.Fatal(err) } } if !has() { t.Fatal("remembered after registering") } if err := r.SetBetaRegistered(ctx, g, false); err != nil || has() { t.Fatalf("forgotten after removing: %v", err) } if err := r.SetBetaRegistered(ctx, "not-a-number", true); err == nil { t.Error("a bad ID is refused") } }