From 7a0bc1f815a78c25314f82af9a72dc9cec06987e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 30 Apr 2026 17:42:55 -0400 Subject: [PATCH] fix(lidarrquarantine): translate FK violation to ErrTrackNotFound + drop unneeded *string AlbumTitle - Flag now detects Postgres SQLSTATE 23503 (foreign_key_violation) on UpsertQuarantine and surfaces ErrTrackNotFound instead of a wrapped generic. T8's handler can now return 404 track_not_found cleanly. Constraint-name guard ('track' substring) keeps a future user_id FK from mis-mapping to the same error. - AdminQueueRow.AlbumTitle is now string (not *string). albums.title is NOT NULL upstream; the empty-string-to-nil dance was answering a nullability question that doesn't exist in the schema. - Add TestFlag_NonexistentTrackReturnsErrTrackNotFound covering the new branch. --- internal/lidarrquarantine/service.go | 24 ++++++++++++----------- internal/lidarrquarantine/service_test.go | 16 +++++++++++++++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/internal/lidarrquarantine/service.go b/internal/lidarrquarantine/service.go index e69cb001..b13343b6 100644 --- a/internal/lidarrquarantine/service.go +++ b/internal/lidarrquarantine/service.go @@ -9,8 +9,10 @@ import ( "context" "errors" "fmt" + "strings" "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" @@ -64,10 +66,15 @@ func (s *Service) Flag(ctx context.Context, userID, trackID pgtype.UUID, reason Notes: notesPtr, }) if err != nil { - // FK violation on track_id surfaces here as a postgres error; - // callers can treat any error as "track doesn't exist or DB issue." - // Distinguishing FK violation specifically isn't worth the extra - // dependency on pgconn just for one branch. + // FK violation on track_id (no such track) → ErrTrackNotFound so the + // handler can return 404 instead of a generic 500. Code 23503 is the + // Postgres SQLSTATE for foreign_key_violation; the constraint check + // guards against future FKs (e.g. user_id) firing the same code. + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) && pgErr.Code == "23503" && + (pgErr.ConstraintName == "" || strings.Contains(pgErr.ConstraintName, "track")) { + return dbq.LidarrQuarantine{}, ErrTrackNotFound + } return dbq.LidarrQuarantine{}, fmt.Errorf("upsert: %w", err) } return row, nil @@ -102,7 +109,7 @@ type AdminQueueRow struct { TrackID pgtype.UUID TrackTitle string ArtistName string - AlbumTitle *string + AlbumTitle string AlbumID pgtype.UUID LidarrAlbumMBID *string ReportCount int32 @@ -149,16 +156,11 @@ func (s *Service) ListAdminQueue(ctx context.Context) ([]AdminQueueRow, error) { CreatedAt: rep.CreatedAt, }) } - var albumTitle *string - if r.AlbumTitle != "" { - t := r.AlbumTitle - albumTitle = &t - } out = append(out, AdminQueueRow{ TrackID: r.TrackID, TrackTitle: r.TrackTitle, ArtistName: r.ArtistName, - AlbumTitle: albumTitle, + AlbumTitle: r.AlbumTitle, AlbumID: r.AlbumID, LidarrAlbumMBID: r.LidarrAlbumMbid, ReportCount: r.ReportCount, diff --git a/internal/lidarrquarantine/service_test.go b/internal/lidarrquarantine/service_test.go index 57862d72..8d6b4ba7 100644 --- a/internal/lidarrquarantine/service_test.go +++ b/internal/lidarrquarantine/service_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" + "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgxpool" "git.fabledsword.com/bvandeusen/minstrel/internal/db" @@ -117,6 +118,21 @@ func TestFlag_UpsertOnSecondFlag(t *testing.T) { } } +func TestFlag_NonexistentTrackReturnsErrTrackNotFound(t *testing.T) { + pool := newPool(t) + user := seedUser(t, pool, "alice") + + var bogus pgtype.UUID + bogus.Bytes = [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} + bogus.Valid = true + + svc := NewService(pool, lidarrconfig.New(pool), nil) + _, err := svc.Flag(context.Background(), user.ID, bogus, "bad_rip", "") + if !errors.Is(err, ErrTrackNotFound) { + t.Errorf("err = %v, want ErrTrackNotFound", err) + } +} + func TestFlag_BadReasonRejected(t *testing.T) { pool := newPool(t) user := seedUser(t, pool, "alice")