test: add registered data-fix effect tests
This commit is contained in:
183
internal/database/fixes/fixes_test.go
Normal file
183
internal/database/fixes/fixes_test.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package fixes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func TestEpisodeAvailabilityBackfillSetsMissingNextRefreshAt(t *testing.T) {
|
||||
sqlDB := newFixTestDB(t)
|
||||
defer closeTestDB(t, sqlDB)
|
||||
|
||||
ctx := context.Background()
|
||||
if _, err := sqlDB.ExecContext(ctx, `INSERT INTO episode_availability_cache (anime_id, data, next_refresh_at, updated_at) VALUES (1, '{}', NULL, '2000-01-01T00:00:00Z')`); err != nil {
|
||||
t.Fatalf("insert missing next_refresh row: %v", err)
|
||||
}
|
||||
if _, err := sqlDB.ExecContext(ctx, `INSERT INTO episode_availability_cache (anime_id, data, next_refresh_at, updated_at) VALUES (2, '{}', '2999-01-01T00:00:00Z', '2000-01-01T00:00:00Z')`); err != nil {
|
||||
t.Fatalf("insert populated next_refresh row: %v", err)
|
||||
}
|
||||
|
||||
runFix(ctx, t, sqlDB, "20260526_episode_availability_backfill_next_refresh_at", Dependencies{})
|
||||
|
||||
assertNextRefreshAtBackfilled(ctx, t, sqlDB, 1)
|
||||
assertUpdatedAtChanged(ctx, t, sqlDB, 1, "2000-01-01T00:00:00Z")
|
||||
assertNextRefreshAtEquals(ctx, t, sqlDB, 2, "2999-01-01T00:00:00Z")
|
||||
|
||||
backfilledNextRefreshAt := queryNextRefreshAt(ctx, t, sqlDB, 1)
|
||||
|
||||
runFix(ctx, t, sqlDB, "20260526_episode_availability_backfill_next_refresh_at", Dependencies{})
|
||||
assertNextRefreshAtEquals(ctx, t, sqlDB, 1, backfilledNextRefreshAt)
|
||||
}
|
||||
|
||||
func TestAvatarURLBackfillUsesDefaultAvatarForBlankURLs(t *testing.T) {
|
||||
sqlDB := newFixTestDB(t)
|
||||
defer closeTestDB(t, sqlDB)
|
||||
|
||||
ctx := context.Background()
|
||||
if _, err := sqlDB.ExecContext(ctx, `INSERT INTO user (id, username, password_hash, avatar_url) VALUES ('blank', 'alice', 'hash', '')`); err != nil {
|
||||
t.Fatalf("insert blank avatar user: %v", err)
|
||||
}
|
||||
if _, err := sqlDB.ExecContext(ctx, `INSERT INTO user (id, username, password_hash, avatar_url) VALUES ('existing', 'bob', 'hash', 'custom.png')`); err != nil {
|
||||
t.Fatalf("insert existing avatar user: %v", err)
|
||||
}
|
||||
|
||||
deps := Dependencies{DefaultAvatarURL: func(username string) string { return "avatar/" + username + ".png" }}
|
||||
runFix(ctx, t, sqlDB, "20260528_backfill_avatar_url", deps)
|
||||
|
||||
assertUserAvatarURL(ctx, t, sqlDB, "blank", "avatar/alice.png")
|
||||
assertUserAvatarURL(ctx, t, sqlDB, "existing", "custom.png")
|
||||
|
||||
runFix(ctx, t, sqlDB, "20260528_backfill_avatar_url", deps)
|
||||
assertUserAvatarURL(ctx, t, sqlDB, "blank", "avatar/alice.png")
|
||||
}
|
||||
|
||||
func TestListAnimeMissingDurationSecondsOnlyReturnsRowsWithNullDuration(t *testing.T) {
|
||||
sqlDB := newFixTestDB(t)
|
||||
defer closeTestDB(t, sqlDB)
|
||||
|
||||
ctx := context.Background()
|
||||
if _, err := sqlDB.ExecContext(ctx, `INSERT INTO anime (id, title_original, image_url, duration_seconds) VALUES (1, 'Missing', 'missing.png', NULL)`); err != nil {
|
||||
t.Fatalf("insert missing duration anime: %v", err)
|
||||
}
|
||||
if _, err := sqlDB.ExecContext(ctx, `INSERT INTO anime (id, title_original, image_url, duration_seconds) VALUES (2, 'Present', 'present.png', 1440)`); err != nil {
|
||||
t.Fatalf("insert present duration anime: %v", err)
|
||||
}
|
||||
|
||||
rows, err := listAnimeMissingDurationSeconds(ctx, sqlDB)
|
||||
if err != nil {
|
||||
t.Fatalf("listAnimeMissingDurationSeconds: %v", err)
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("missing duration rows = %v, want 1 row", rows)
|
||||
}
|
||||
if rows[0].id != 1 || rows[0].titleOriginal != "Missing" {
|
||||
t.Fatalf("missing duration row = %+v, want anime 1", rows[0])
|
||||
}
|
||||
}
|
||||
|
||||
func runFix(ctx context.Context, t *testing.T, sqlDB *sql.DB, id string, deps Dependencies) {
|
||||
t.Helper()
|
||||
|
||||
for _, fix := range All() {
|
||||
if fix.ID != id {
|
||||
continue
|
||||
}
|
||||
if err := fix.Apply(ctx, sqlDB, deps); err != nil {
|
||||
t.Fatalf("apply fix %s: %v", id, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("fix %s not registered", id)
|
||||
}
|
||||
|
||||
func queryNextRefreshAt(ctx context.Context, t *testing.T, sqlDB *sql.DB, animeID int64) string {
|
||||
t.Helper()
|
||||
|
||||
var v sql.NullString
|
||||
if err := sqlDB.QueryRowContext(ctx, `SELECT next_refresh_at FROM episode_availability_cache WHERE anime_id = ?`, animeID).Scan(&v); err != nil {
|
||||
t.Fatalf("query next_refresh_at for %d: %v", animeID, err)
|
||||
}
|
||||
if !v.Valid || v.String == "" {
|
||||
t.Fatalf("next_refresh_at for %d = %q, valid=%v; want populated", animeID, v.String, v.Valid)
|
||||
}
|
||||
return v.String
|
||||
}
|
||||
|
||||
func assertNextRefreshAtBackfilled(ctx context.Context, t *testing.T, sqlDB *sql.DB, animeID int64) {
|
||||
t.Helper()
|
||||
queryNextRefreshAt(ctx, t, sqlDB, animeID)
|
||||
}
|
||||
|
||||
func assertUpdatedAtChanged(ctx context.Context, t *testing.T, sqlDB *sql.DB, animeID int64, original string) {
|
||||
t.Helper()
|
||||
|
||||
var updatedAt string
|
||||
if err := sqlDB.QueryRowContext(ctx, `SELECT updated_at FROM episode_availability_cache WHERE anime_id = ?`, animeID).Scan(&updatedAt); err != nil {
|
||||
t.Fatalf("query updated_at for %d: %v", animeID, err)
|
||||
}
|
||||
if updatedAt == original {
|
||||
t.Fatalf("updated_at for %d = %q, expected change", animeID, updatedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func assertNextRefreshAtEquals(ctx context.Context, t *testing.T, sqlDB *sql.DB, animeID int64, want string) {
|
||||
t.Helper()
|
||||
|
||||
var v sql.NullString
|
||||
if err := sqlDB.QueryRowContext(ctx, `SELECT next_refresh_at FROM episode_availability_cache WHERE anime_id = ?`, animeID).Scan(&v); err != nil {
|
||||
t.Fatalf("query next_refresh_at for %d: %v", animeID, err)
|
||||
}
|
||||
if !v.Valid {
|
||||
t.Fatalf("next_refresh_at for %d is NULL", animeID)
|
||||
}
|
||||
if v.String != want {
|
||||
t.Fatalf("next_refresh_at for %d = %q, want %q", animeID, v.String, want)
|
||||
}
|
||||
}
|
||||
|
||||
func assertUserAvatarURL(ctx context.Context, t *testing.T, sqlDB *sql.DB, id string, want string) {
|
||||
t.Helper()
|
||||
|
||||
var got string
|
||||
if err := sqlDB.QueryRowContext(ctx, `SELECT avatar_url FROM user WHERE id = ?`, id).Scan(&got); err != nil {
|
||||
t.Fatalf("query avatar_url for %s: %v", id, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("avatar_url for %s = %q, want %q", id, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func newFixTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
|
||||
sqlDB, err := sql.Open("sqlite3", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(1)
|
||||
|
||||
ctx := context.Background()
|
||||
for _, statement := range []string{
|
||||
`CREATE TABLE user (id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, avatar_url TEXT NOT NULL DEFAULT '')`,
|
||||
`CREATE TABLE anime (id INTEGER PRIMARY KEY, title_original TEXT NOT NULL, title_english TEXT, title_japanese TEXT, image_url TEXT NOT NULL, airing BOOLEAN, duration_seconds REAL)`,
|
||||
`CREATE TABLE episode_availability_cache (anime_id INTEGER PRIMARY KEY, data TEXT NOT NULL, next_refresh_at DATETIME, retry_until_at DATETIME, last_attempt_at DATETIME, last_success_at DATETIME, failure_count INTEGER NOT NULL DEFAULT 0, last_error TEXT NOT NULL DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
|
||||
} {
|
||||
if _, err := sqlDB.ExecContext(ctx, statement); err != nil {
|
||||
closeTestDB(t, sqlDB)
|
||||
t.Fatalf("create test schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return sqlDB
|
||||
}
|
||||
|
||||
func closeTestDB(t *testing.T, sqlDB *sql.DB) {
|
||||
t.Helper()
|
||||
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
t.Errorf("close sqlite: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user