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.
This commit is contained in:
2026-04-30 17:42:55 -04:00
parent b4fe224a67
commit 7a0bc1f815
2 changed files with 29 additions and 11 deletions
+13 -11
View File
@@ -9,8 +9,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
@@ -64,10 +66,15 @@ func (s *Service) Flag(ctx context.Context, userID, trackID pgtype.UUID, reason
Notes: notesPtr, Notes: notesPtr,
}) })
if err != nil { if err != nil {
// FK violation on track_id surfaces here as a postgres error; // FK violation on track_id (no such track) → ErrTrackNotFound so the
// callers can treat any error as "track doesn't exist or DB issue." // handler can return 404 instead of a generic 500. Code 23503 is the
// Distinguishing FK violation specifically isn't worth the extra // Postgres SQLSTATE for foreign_key_violation; the constraint check
// dependency on pgconn just for one branch. // 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 dbq.LidarrQuarantine{}, fmt.Errorf("upsert: %w", err)
} }
return row, nil return row, nil
@@ -102,7 +109,7 @@ type AdminQueueRow struct {
TrackID pgtype.UUID TrackID pgtype.UUID
TrackTitle string TrackTitle string
ArtistName string ArtistName string
AlbumTitle *string AlbumTitle string
AlbumID pgtype.UUID AlbumID pgtype.UUID
LidarrAlbumMBID *string LidarrAlbumMBID *string
ReportCount int32 ReportCount int32
@@ -149,16 +156,11 @@ func (s *Service) ListAdminQueue(ctx context.Context) ([]AdminQueueRow, error) {
CreatedAt: rep.CreatedAt, CreatedAt: rep.CreatedAt,
}) })
} }
var albumTitle *string
if r.AlbumTitle != "" {
t := r.AlbumTitle
albumTitle = &t
}
out = append(out, AdminQueueRow{ out = append(out, AdminQueueRow{
TrackID: r.TrackID, TrackID: r.TrackID,
TrackTitle: r.TrackTitle, TrackTitle: r.TrackTitle,
ArtistName: r.ArtistName, ArtistName: r.ArtistName,
AlbumTitle: albumTitle, AlbumTitle: r.AlbumTitle,
AlbumID: r.AlbumID, AlbumID: r.AlbumID,
LidarrAlbumMBID: r.LidarrAlbumMbid, LidarrAlbumMBID: r.LidarrAlbumMbid,
ReportCount: r.ReportCount, ReportCount: r.ReportCount,
+16
View File
@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/jackc/pgx/v5/pgtype"
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"git.fabledsword.com/bvandeusen/minstrel/internal/db" "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) { func TestFlag_BadReasonRejected(t *testing.T) {
pool := newPool(t) pool := newPool(t)
user := seedUser(t, pool, "alice") user := seedUser(t, pool, "alice")