test: add registered data-fix effect tests

This commit is contained in:
2026-06-27 16:31:05 +02:00
parent 531566291b
commit ae00c04cf9

View 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)
}
}