package db_test import ( "context" "errors" "path/filepath" "strings" "testing" "time" "go.uber.org/fx/fxtest" "sneak.berlin/go/neoirc/internal/config" "sneak.berlin/go/neoirc/internal/db" "sneak.berlin/go/neoirc/internal/globals" "sneak.berlin/go/neoirc/internal/logger" ) const wantBusyTimeoutMS = 5000 const ( // endlessQuery counts the rows of a recursive query that never // stops, so it runs until its context ends. endlessQuery = `WITH RECURSIVE n(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM n) SELECT COUNT(*) FROM n` endlessQueryTimeout = 100 * time.Millisecond ) // TestDefaultDBURLSetsWALAndBusyTimeout opens a file database // with the default URL's parameters the way the server does, // and checks the journal mode and busy timeout it ends up with. func TestDefaultDBURLSetsWALAndBusyTimeout(t *testing.T) { t.Parallel() _, params, _ := strings.Cut(config.DefaultDBURL, "?") dbURL := "file://" + filepath.Join(t.TempDir(), "state.db") + "?" + params lifecycle := fxtest.NewLifecycle(t) log, err := logger.New(lifecycle, logger.Params{ Globals: &globals.Globals{}, }) if err != nil { t.Fatal(err) } database, err := db.New(lifecycle, db.Params{ Logger: log, Config: &config.Config{DBURL: dbURL}, }) if err != nil { t.Fatal(err) } lifecycle.RequireStart() t.Cleanup(lifecycle.RequireStop) var journalMode string err = database.GetDB().QueryRowContext( t.Context(), "PRAGMA journal_mode", ).Scan(&journalMode) if err != nil { t.Fatal(err) } if journalMode != "wal" { t.Errorf("journal_mode = %q, want %q", journalMode, "wal") } var busyTimeout int err = database.GetDB().QueryRowContext( t.Context(), "PRAGMA busy_timeout", ).Scan(&busyTimeout) if err != nil { t.Fatal(err) } if busyTimeout != wantBusyTimeoutMS { t.Errorf( "busy_timeout = %d, want %d", busyTimeout, wantBusyTimeoutMS, ) } } // TestForeignKeysOnAfterCancelledQuery cancels a query while it // runs, which makes the pool close its only connection and open a // new one. It checks that the file database keeps its tables and // that the new connection has foreign keys on. func TestForeignKeysOnAfterCancelledQuery(t *testing.T) { t.Parallel() _, defaultParams, _ := strings.Cut(config.DefaultDBURL, "?") urlParams := map[string]string{ "default parameters": "?" + defaultParams, "no parameters": "", } for name, params := range urlParams { t.Run(name, func(t *testing.T) { t.Parallel() dbURL := "file://" + filepath.Join(t.TempDir(), "state.db") + params lifecycle := fxtest.NewLifecycle(t) log, err := logger.New(lifecycle, logger.Params{ Globals: &globals.Globals{}, }) if err != nil { t.Fatal(err) } database, err := db.New(lifecycle, db.Params{ Logger: log, Config: &config.Config{DBURL: dbURL}, }) if err != nil { t.Fatal(err) } lifecycle.RequireStart() t.Cleanup(lifecycle.RequireStop) ctx, cancel := context.WithTimeout( t.Context(), endlessQueryTimeout, ) defer cancel() _, err = database.GetDB().ExecContext(ctx, endlessQuery) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("endless query: err = %v", err) } var migrations int err = database.GetDB().QueryRowContext( t.Context(), "SELECT COUNT(*) FROM schema_migrations", ).Scan(&migrations) if err != nil { t.Fatal(err) } if migrations == 0 { t.Error("schema_migrations is empty") } var foreignKeys int err = database.GetDB().QueryRowContext( t.Context(), "PRAGMA foreign_keys", ).Scan(&foreignKeys) if err != nil { t.Fatal(err) } if foreignKeys != 1 { t.Errorf("foreign_keys = %d, want 1", foreignKeys) } }) } }