package component import ( "context" "errors" "testing" "github.com/jackc/pgx/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "homelab.lan/music-agregator/internal" "homelab.lan/music-agregator/internal/database" ) func insertTestArtistAndAlbum(t *testing.T, suite *testSuite) string { ctx := context.Background() var artistID string err := suite.pool.QueryRow(ctx, ` INSERT INTO artists (external_id, name, artist_type, country, genres, image_url, monitor_state) VALUES ('test-artist-ext', 'Test Artist', 'person', 'US', ARRAY['Rock'], 'http://img.com', 'monitored') RETURNING id `).Scan(&artistID) require.NoError(t, err) var albumID string err = suite.pool.QueryRow(ctx, ` INSERT INTO albums (external_id, artist_id, title, album_type, total_tracks, total_discs, label, genres, cover_url, monitor_state) VALUES ('test-album-ext', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored') RETURNING id `, artistID).Scan(&albumID) require.NoError(t, err) return albumID } func TestWorkflowRun_Create(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{ AlbumID: albumID, Quality: "flac", } err := repo.Create(ctx, run) require.NoError(t, err) assert.NotEmpty(t, run.ID) assert.False(t, run.StartedAt.IsZero()) fetched, err := repo.GetByID(ctx, run.ID) require.NoError(t, err) assert.Equal(t, "running", fetched.Status) } func TestWorkflowRun_DuplicateRunningRejected(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run1 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run1) require.NoError(t, err) run2 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err = repo.Create(ctx, run2) assert.ErrorIs(t, err, database.ErrWorkflowAlreadyRunning) } func TestWorkflowRun_DuplicateAllowedAfterCompletion(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run1 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run1) require.NoError(t, err) err = repo.SetCompleted(ctx, run1.ID) require.NoError(t, err) run2 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err = repo.Create(ctx, run2) require.NoError(t, err) assert.NotEmpty(t, run2.ID) assert.NotEqual(t, run1.ID, run2.ID) } func TestWorkflowRun_SetCompleted(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run) require.NoError(t, err) err = repo.SetCompleted(ctx, run.ID) require.NoError(t, err) updated, err := repo.GetByID(ctx, run.ID) require.NoError(t, err) assert.Equal(t, "completed", updated.Status) assert.NotNil(t, updated.CompletedAt) } func TestWorkflowRun_SetFailed(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run) require.NoError(t, err) err = repo.SetFailed(ctx, run.ID, "something went wrong") require.NoError(t, err) updated, err := repo.GetByID(ctx, run.ID) require.NoError(t, err) assert.Equal(t, "failed", updated.Status) require.NotNil(t, updated.ErrorMessage) assert.Equal(t, "something went wrong", *updated.ErrorMessage) } func TestWorkflowRun_SetCancelled(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run) require.NoError(t, err) err = repo.SetCancelled(ctx, run.ID) require.NoError(t, err) updated, err := repo.GetByID(ctx, run.ID) require.NoError(t, err) assert.Equal(t, "cancelled", updated.Status) } func TestWorkflowRun_GetByAlbumAndQuality(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run) require.NoError(t, err) found, err := repo.GetByAlbumAndQuality(ctx, albumID, "flac") require.NoError(t, err) assert.Equal(t, run.ID, found.ID) _, err = repo.GetByAlbumAndQuality(ctx, albumID, "mp3") assert.True(t, errors.Is(err, pgx.ErrNoRows) || err != nil) } func TestWorkflowRun_GetRunning(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) repo := database.NewWorkflowRunRepository(suite.pool) run1 := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := repo.Create(ctx, run1) require.NoError(t, err) run2 := &database.WorkflowRun{AlbumID: albumID, Quality: "mp3"} err = repo.Create(ctx, run2) require.NoError(t, err) run3 := &database.WorkflowRun{AlbumID: albumID, Quality: "opus"} err = repo.Create(ctx, run3) require.NoError(t, err) err = repo.SetCompleted(ctx, run3.ID) require.NoError(t, err) running, err := repo.GetRunning(ctx) require.NoError(t, err) assert.Len(t, running, 2) ids := []string{running[0].ID, running[1].ID} assert.Contains(t, ids, run1.ID) assert.Contains(t, ids, run2.ID) } func TestAlbumEvent_Create(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) workflowRepo := database.NewWorkflowRunRepository(suite.pool) eventRepo := database.NewAlbumEventRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := workflowRepo.Create(ctx, run) require.NoError(t, err) event := &database.AlbumEvent{ WorkflowRunID: run.ID, AlbumID: albumID, EventType: "info", Step: "searching", Message: "started search", DataJSON: []byte(`{"query":"test"}`), } err = eventRepo.Create(ctx, event) require.NoError(t, err) assert.NotEmpty(t, event.ID) assert.Greater(t, event.Seq, int64(0)) } func TestAlbumEvent_GetByWorkflowRun(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) workflowRepo := database.NewWorkflowRunRepository(suite.pool) eventRepo := database.NewAlbumEventRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := workflowRepo.Create(ctx, run) require.NoError(t, err) for i := 0; i < 3; i++ { event := &database.AlbumEvent{ WorkflowRunID: run.ID, AlbumID: albumID, EventType: "info", Step: "step", Message: "msg", } err = eventRepo.Create(ctx, event) require.NoError(t, err) } events, err := eventRepo.GetByWorkflowRun(ctx, run.ID) require.NoError(t, err) assert.Len(t, events, 3) assert.Less(t, events[0].Seq, events[1].Seq) assert.Less(t, events[1].Seq, events[2].Seq) } func TestAlbumEvent_GetByAlbum(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) workflowRepo := database.NewWorkflowRunRepository(suite.pool) eventRepo := database.NewAlbumEventRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := workflowRepo.Create(ctx, run) require.NoError(t, err) var seqs []int64 for i := 0; i < 5; i++ { event := &database.AlbumEvent{ WorkflowRunID: run.ID, AlbumID: albumID, EventType: "info", Step: "step", Message: "msg", } err = eventRepo.Create(ctx, event) require.NoError(t, err) seqs = append(seqs, event.Seq) } events, err := eventRepo.GetByAlbum(ctx, albumID, seqs[1], 10) require.NoError(t, err) assert.Len(t, events, 3) assert.Equal(t, seqs[2], events[0].Seq) assert.Equal(t, seqs[3], events[1].Seq) assert.Equal(t, seqs[4], events[2].Seq) } func TestAlbumEvent_GetLatestSeq(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() eventRepo := database.NewAlbumEventRepository(suite.pool) seq, err := eventRepo.GetLatestSeq(ctx) require.NoError(t, err) assert.Equal(t, int64(0), seq) albumID := insertTestArtistAndAlbum(t, suite) workflowRepo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err = workflowRepo.Create(ctx, run) require.NoError(t, err) var lastSeq int64 for i := 0; i < 3; i++ { event := &database.AlbumEvent{ WorkflowRunID: run.ID, AlbumID: albumID, EventType: "info", Step: "step", Message: "msg", } err = eventRepo.Create(ctx, event) require.NoError(t, err) lastSeq = event.Seq } seq, err = eventRepo.GetLatestSeq(ctx) require.NoError(t, err) assert.Equal(t, lastSeq, seq) } func TestRecovery_StaleWorkflowWithDownload(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) workflowRepo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := workflowRepo.Create(ctx, run) require.NoError(t, err) _, err = suite.pool.Exec(ctx, ` UPDATE workflow_runs SET started_at = NOW() - INTERVAL '10 minutes' WHERE id = $1 `, run.ID) require.NoError(t, err) var torrentID string err = suite.pool.QueryRow(ctx, ` INSERT INTO torrents (album_id, info_hash, tracker, title, format, quality, source, seeders, peers, size) VALUES ($1, 'test-hash', 'test-tracker', 'Test Torrent', 'FLAC', '16-44', 'CD', 100, 50, 500000000) RETURNING id `, albumID).Scan(&torrentID) require.NoError(t, err) _, err = suite.pool.Exec(ctx, ` INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash) VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'test-hash') `, torrentID, albumID) require.NoError(t, err) service := createRecoveryTestService(suite) service.RecoverWorkflows(ctx) updated, err := workflowRepo.GetByID(ctx, run.ID) require.NoError(t, err) assert.Equal(t, "completed", updated.Status) } func TestRecovery_StaleWorkflowWithoutDownload(t *testing.T) { suite := setupSuite(t) cleanTables(t, suite.pool) ctx := context.Background() albumID := insertTestArtistAndAlbum(t, suite) workflowRepo := database.NewWorkflowRunRepository(suite.pool) run := &database.WorkflowRun{AlbumID: albumID, Quality: "flac"} err := workflowRepo.Create(ctx, run) require.NoError(t, err) _, err = suite.pool.Exec(ctx, ` UPDATE workflow_runs SET started_at = NOW() - INTERVAL '10 minutes' WHERE id = $1 `, run.ID) require.NoError(t, err) service := createRecoveryTestService(suite) service.RecoverWorkflows(ctx) updated, err := workflowRepo.GetByID(ctx, run.ID) require.NoError(t, err) assert.Equal(t, "failed", updated.Status) require.NotNil(t, updated.ErrorMessage) assert.Contains(t, *updated.ErrorMessage, "server restarted") } func createRecoveryTestService(suite *testSuite) *internal.MusicAgregatorService { return internal.NewMusicAgregatorServiceWithDeps( nil, nil, nil, nil, nil, nil, suite.db, ) }