- LICENSE (Apache-2.0), NOTICE, CONTRIBUTING.md - docs/self-hosting.md and README sections for hosting and contributing - Close() no longer crashes: it only disconnects and leaves the slash commands registered, so they keep their IDs across restarts - Tests use a neutral server ID Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
259 lines
6.9 KiB
Go
259 lines
6.9 KiB
Go
//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, 700000000000000001)`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, role, err = r.GetBetaSettings(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if g != "700000000000000001" {
|
|
t.Fatalf("g = %q, want %q", g, "700000000000000001")
|
|
}
|
|
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")
|
|
}
|
|
}
|