Files
music-agregator/test/component/monitor_album_test.go
T

1134 lines
38 KiB
Go

package component
import (
"context"
"os"
"path/filepath"
"testing"
"github.com/riverqueue/river"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
"homelab.lan/music-agregator/internal/database"
"homelab.lan/music-agregator/internal/indexer"
"homelab.lan/music-agregator/internal/torrent"
"homelab.lan/music-agregator/internal/workers"
)
func TestMonitorAlbum_HappyPath(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Artist - Test Album [FLAC]", 50, "magnet:?xt=urn:btih:abc123&dn=test"),
newSearchItem("Test Artist - Test Album [MP3 320]", 10, "magnet:?xt=urn:btih:def456&dn=test"),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentData(), nil
}
suite.mocks.torrent.FindFunc = func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{}, nil
}
suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error {
return nil
}
ctx := context.Background()
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Album)
assert.NotEmpty(t, resp.Album.Id)
assert.Equal(t, "Test Album", resp.Album.Title)
assert.Equal(t, pb.MonitorState_MONITOR_STATE_MONITORED, resp.Album.MonitorState)
require.NotNil(t, resp.Artist)
assert.NotEmpty(t, resp.Artist.Id)
assert.Equal(t, "Test Artist", resp.Artist.Name)
assert.Equal(t, pb.MonitorState_MONITOR_STATE_MONITORED, resp.Artist.MonitorState)
require.NotNil(t, resp.Release)
assert.NotEmpty(t, resp.Release.InfoHash)
assert.Equal(t, int32(50), resp.Release.Seeders)
var artistMonitorState string
err = suite.pool.QueryRow(ctx, "SELECT monitor_state FROM artists WHERE external_id = $1", "artist-ext-id").Scan(&artistMonitorState)
require.NoError(t, err)
assert.Equal(t, "monitored", artistMonitorState)
var albumMonitorState string
err = suite.pool.QueryRow(ctx, "SELECT monitor_state FROM albums WHERE external_id = $1", "test-album-ext-id").Scan(&albumMonitorState)
require.NoError(t, err)
assert.Equal(t, "monitored", albumMonitorState)
var torrentCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&torrentCount)
require.NoError(t, err)
assert.Equal(t, 1, torrentCount)
var downloadState string
err = suite.pool.QueryRow(ctx, "SELECT state FROM downloads").Scan(&downloadState)
require.NoError(t, err)
assert.Equal(t, "downloading", downloadState)
}
func TestMonitorAlbum_MetadataUnavailable(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return nil, status.Error(codes.Unavailable, "connection refused")
}
ctx := context.Background()
_, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.Error(t, err)
var count int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM artists").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestMonitorAlbum_MetadataNotFound(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return nil, status.Error(codes.NotFound, "album not found")
}
ctx := context.Background()
_, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "nonexistent-album",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.Error(t, err)
var count int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM artists").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestMonitorAlbum_ArtistPersistFails(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: &metadataPb.Album{
Id: "orphan-album-ext-id",
Title: "Orphan Album",
AlbumType: "album",
ReleaseDate: "2024-01-15",
TotalTracks: 10,
Artists: []*metadataPb.ArtistCredit{},
},
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Orphan Album [FLAC]", 50, "magnet:?xt=urn:btih:abc123&dn=test"),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentData(), nil
}
suite.mocks.torrent.FindFunc = func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{}, nil
}
suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error {
return nil
}
ctx := context.Background()
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "orphan-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Release)
assert.Nil(t, resp.Album)
var count int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestMonitorAlbum_AlreadyOwned(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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 ('owned-album-ext-id', $1, 'Owned Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'existing-hash', 'test-tracker', 'Owned Album FLAC', '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, save_path)
VALUES ($1, $2, 'QUALITY_LOSSLESS', '16-44', 'completed', 'existing-hash', '/music/owned')
`, torrentID, albumID)
require.NoError(t, err)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("owned-album-ext-id", "Owned Album", "artist-ext-id", "Test Artist"),
}, nil
}
var indexerCalled bool
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
indexerCalled = true
return newSearchResponse(), nil
}
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "owned-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Album)
assert.Equal(t, pb.MonitorState_MONITOR_STATE_MONITORED, resp.Album.MonitorState)
require.NotNil(t, resp.Album.Download)
assert.Equal(t, "completed", resp.Album.Download.State)
assert.Nil(t, resp.Release)
assert.False(t, indexerCalled)
var downloadCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
require.NoError(t, err)
assert.Equal(t, 1, downloadCount)
}
func TestMonitorAlbum_IndexerDown(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return nil, assert.AnError
}
ctx := context.Background()
_, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.Error(t, err)
var artistMonitorState string
err = suite.pool.QueryRow(ctx, "SELECT monitor_state FROM artists WHERE external_id = $1", "artist-ext-id").Scan(&artistMonitorState)
require.NoError(t, err)
assert.Equal(t, "monitored", artistMonitorState)
var albumMonitorState string
err = suite.pool.QueryRow(ctx, "SELECT monitor_state FROM albums WHERE external_id = $1", "test-album-ext-id").Scan(&albumMonitorState)
require.NoError(t, err)
assert.Equal(t, "monitored", albumMonitorState)
}
func TestMonitorAlbum_IndexerNoResults(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(), nil
}
ctx := context.Background()
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Album)
require.NotNil(t, resp.Artist)
assert.Nil(t, resp.Release)
var count int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&count)
require.NoError(t, err)
assert.Equal(t, 0, count)
}
func TestMonitorAlbum_AllSeedersZero(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album FLAC 1", 0, "magnet:?xt=urn:btih:abc1"),
newSearchItem("Test Album FLAC 2", 0, "magnet:?xt=urn:btih:abc2"),
newSearchItem("Test Album FLAC 3", 0, "magnet:?xt=urn:btih:abc3"),
), nil
}
ctx := context.Background()
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Album)
require.NotNil(t, resp.Artist)
assert.Nil(t, resp.Release)
}
func TestMonitorAlbum_AllMagnetsFail(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album 1", 50, "magnet:?xt=urn:btih:abc1"),
newSearchItem("Test Album 2", 30, "magnet:?xt=urn:btih:abc2"),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return nil, assert.AnError
}
ctx := context.Background()
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Album)
require.NotNil(t, resp.Artist)
assert.Nil(t, resp.Release)
}
func TestMonitorAlbum_NoQualityMatch(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album MP3 320", 50, "magnet:?xt=urn:btih:abc1"),
newSearchItem("Test Album MP3 V0", 30, "magnet:?xt=urn:btih:abc2"),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentDataMP3(), nil
}
ctx := context.Background()
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Album)
require.NotNil(t, resp.Artist)
assert.Nil(t, resp.Release)
}
func TestMonitorAlbum_QBitDown(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album [FLAC]", 50, "magnet:?xt=urn:btih:abc123"),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentData(), nil
}
suite.mocks.torrent.FindFunc = func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return nil, assert.AnError
}
suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error {
return assert.AnError
}
ctx := context.Background()
_, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.Error(t, err)
var artistCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM artists").Scan(&artistCount)
require.NoError(t, err)
assert.Equal(t, 1, artistCount)
var albumCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&albumCount)
require.NoError(t, err)
assert.Equal(t, 1, albumCount)
var torrentCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&torrentCount)
require.NoError(t, err)
assert.Equal(t, 0, torrentCount)
var downloadCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
require.NoError(t, err)
assert.Equal(t, 0, downloadCount)
}
func TestMonitorAlbum_TorrentAlreadyExists(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'unmonitored')
RETURNING id
`, artistID).Scan(&albumID)
require.NoError(t, err)
torrentHash := "6ff7af15d0745a3e29d1b9620191cfe01ad3cc70"
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, $2, 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID, torrentHash).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', 'completed', $3)
`, torrentID, albumID, torrentHash)
require.NoError(t, err)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album FLAC", 50, "magnet:?xt=urn:btih:"+torrentHash),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentData(), nil
}
suite.mocks.torrent.FindFunc = func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{{State: "stalledUP"}}, nil
}
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Release)
var downloadCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
require.NoError(t, err)
assert.Equal(t, 1, downloadCount)
}
func TestMonitorAlbum_AddMagnetFails(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album FLAC", 50, "magnet:?xt=urn:btih:abc123"),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentData(), nil
}
suite.mocks.torrent.FindFunc = func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{}, nil
}
suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error {
return assert.AnError
}
ctx := context.Background()
_, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.Error(t, err)
var albumCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&albumCount)
require.NoError(t, err)
assert.Equal(t, 1, albumCount)
var downloadCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
require.NoError(t, err)
assert.Equal(t, 0, downloadCount)
}
func TestMonitorAlbum_DuplicateDownloadSkipped(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'unmonitored')
RETURNING id
`, artistID).Scan(&albumID)
require.NoError(t, err)
torrentHash := "6ff7af15d0745a3e29d1b9620191cfe01ad3cc70"
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, $2, 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID, torrentHash).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', $3)
`, torrentID, albumID, torrentHash)
require.NoError(t, err)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
return newSearchResponse(
newSearchItem("Test Album FLAC", 50, "magnet:?xt=urn:btih:"+torrentHash),
), nil
}
suite.mocks.magnet.ResolveFunc = func(magnetURI string) ([]byte, error) {
return newTorrentData(), nil
}
suite.mocks.torrent.FindFunc = func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{{State: "downloading"}}, nil
}
resp, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
require.NotNil(t, resp.Release)
var downloadCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
require.NoError(t, err)
assert.Equal(t, 1, downloadCount)
}
func TestMonitorAlbum_SearchQueryFormat(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
suite.mocks.metadata.GetAlbumFunc = func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
return &metadataPb.GetAlbumResponse{
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
}, nil
}
var capturedQuery string
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
capturedQuery = query
return newSearchResponse(), nil
}
ctx := context.Background()
_, err := suite.client.MonitorAlbum(ctx, &pb.MonitorAlbumRequest{
AlbumId: "test-album-ext-id",
Quality: pb.QualityType_QUALITY_LOSSLESS,
})
require.NoError(t, err)
assert.Equal(t, "Test Artist Test Album", capturedQuery)
}
func TestPollWorker_QBitUnreachable(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'poll-hash-123', 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID).Scan(&torrentID)
require.NoError(t, err)
var downloadID string
err = suite.pool.QueryRow(ctx, `
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'poll-hash-123')
RETURNING id
`, torrentID, albumID).Scan(&downloadID)
require.NoError(t, err)
mockTorrent := &mockTorrentClient{
FindFunc: func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return nil, assert.AnError
},
}
worker := &workers.PollDownloadWorker{
TorrentClient: mockTorrent,
Downloads: database.NewDownloadRepository(suite.pool),
DownloadFiles: database.NewDownloadFileRepository(suite.pool),
RiverClient: nil,
}
job := &river.Job[workers.PollDownloadArgs]{
Args: workers.PollDownloadArgs{
DownloadID: downloadID,
TorrentHash: "poll-hash-123",
},
}
err = worker.Work(ctx, job)
require.NoError(t, err)
var state string
err = suite.pool.QueryRow(ctx, "SELECT state FROM downloads WHERE id = $1", downloadID).Scan(&state)
require.NoError(t, err)
assert.Equal(t, "downloading", state)
}
func TestPollWorker_TorrentDisappeared(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'disappeared-hash', 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID).Scan(&torrentID)
require.NoError(t, err)
var downloadID string
err = suite.pool.QueryRow(ctx, `
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'disappeared-hash')
RETURNING id
`, torrentID, albumID).Scan(&downloadID)
require.NoError(t, err)
mockTorrent := &mockTorrentClient{
FindFunc: func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{}, nil
},
}
worker := &workers.PollDownloadWorker{
TorrentClient: mockTorrent,
Downloads: database.NewDownloadRepository(suite.pool),
DownloadFiles: database.NewDownloadFileRepository(suite.pool),
RiverClient: nil,
}
job := &river.Job[workers.PollDownloadArgs]{
Args: workers.PollDownloadArgs{
DownloadID: downloadID,
TorrentHash: "disappeared-hash",
},
}
err = worker.Work(ctx, job)
require.NoError(t, err)
var state, errorMsg string
err = suite.pool.QueryRow(ctx, "SELECT state, error_message FROM downloads WHERE id = $1", downloadID).Scan(&state, &errorMsg)
require.NoError(t, err)
assert.Equal(t, "failed", state)
assert.Equal(t, "torrent not found in client", errorMsg)
}
func TestPollWorker_TorrentError(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'error-hash', 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID).Scan(&torrentID)
require.NoError(t, err)
var downloadID string
err = suite.pool.QueryRow(ctx, `
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'error-hash')
RETURNING id
`, torrentID, albumID).Scan(&downloadID)
require.NoError(t, err)
mockTorrent := &mockTorrentClient{
FindFunc: func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{{State: "error"}}, nil
},
}
worker := &workers.PollDownloadWorker{
TorrentClient: mockTorrent,
Downloads: database.NewDownloadRepository(suite.pool),
DownloadFiles: database.NewDownloadFileRepository(suite.pool),
RiverClient: nil,
}
job := &river.Job[workers.PollDownloadArgs]{
Args: workers.PollDownloadArgs{
DownloadID: downloadID,
TorrentHash: "error-hash",
},
}
err = worker.Work(ctx, job)
require.NoError(t, err)
var state, errorMsg string
err = suite.pool.QueryRow(ctx, "SELECT state, error_message FROM downloads WHERE id = $1", downloadID).Scan(&state, &errorMsg)
require.NoError(t, err)
assert.Equal(t, "failed", state)
assert.Equal(t, "torrent error state", errorMsg)
}
func TestPollWorker_CompletedSuccess(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
ctx := context.Background()
tempDir := t.TempDir()
flacFile1 := filepath.Join(tempDir, "01-track.flac")
flacFile2 := filepath.Join(tempDir, "02-track.flac")
require.NoError(t, os.WriteFile(flacFile1, []byte("fake flac data 1"), 0644))
require.NoError(t, os.WriteFile(flacFile2, []byte("fake flac data 2"), 0644))
var artistID string
err := suite.pool.QueryRow(ctx, `
INSERT INTO artists (external_id, name, artist_type, country, genres, image_url, monitor_state)
VALUES ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'completed-hash', 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID).Scan(&torrentID)
require.NoError(t, err)
var downloadID string
err = suite.pool.QueryRow(ctx, `
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'completed-hash')
RETURNING id
`, torrentID, albumID).Scan(&downloadID)
require.NoError(t, err)
mockTorrent := &mockTorrentClient{
FindFunc: func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{{
Progress: 1.0,
SavePath: "/downloads",
ContentPath: tempDir,
}}, nil
},
}
worker := &workers.PollDownloadWorker{
TorrentClient: mockTorrent,
Downloads: database.NewDownloadRepository(suite.pool),
DownloadFiles: database.NewDownloadFileRepository(suite.pool),
RiverClient: nil,
}
job := &river.Job[workers.PollDownloadArgs]{
Args: workers.PollDownloadArgs{
DownloadID: downloadID,
TorrentHash: "completed-hash",
},
}
err = worker.Work(ctx, job)
require.NoError(t, err)
var state string
var savePath *string
err = suite.pool.QueryRow(ctx, "SELECT state, save_path FROM downloads WHERE id = $1", downloadID).Scan(&state, &savePath)
require.NoError(t, err)
assert.Equal(t, "completed", state)
require.NotNil(t, savePath)
assert.Equal(t, "/downloads", *savePath)
var fileCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM download_files WHERE download_id = $1", downloadID).Scan(&fileCount)
require.NoError(t, err)
assert.Equal(t, 2, fileCount)
var fileTypes []string
rows, err := suite.pool.Query(ctx, "SELECT file_type FROM download_files WHERE download_id = $1", downloadID)
require.NoError(t, err)
defer rows.Close()
for rows.Next() {
var ft string
require.NoError(t, rows.Scan(&ft))
fileTypes = append(fileTypes, ft)
}
assert.Contains(t, fileTypes, "flac")
}
func TestPollWorker_FileScanFails(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'scan-fail-hash', 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID).Scan(&torrentID)
require.NoError(t, err)
var downloadID string
err = suite.pool.QueryRow(ctx, `
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'scan-fail-hash')
RETURNING id
`, torrentID, albumID).Scan(&downloadID)
require.NoError(t, err)
mockTorrent := &mockTorrentClient{
FindFunc: func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
return []torrent.TorrentInfo{{
Progress: 1.0,
SavePath: "/downloads",
ContentPath: "/nonexistent/path/that/does/not/exist",
}}, nil
},
}
worker := &workers.PollDownloadWorker{
TorrentClient: mockTorrent,
Downloads: database.NewDownloadRepository(suite.pool),
DownloadFiles: database.NewDownloadFileRepository(suite.pool),
RiverClient: nil,
}
job := &river.Job[workers.PollDownloadArgs]{
Args: workers.PollDownloadArgs{
DownloadID: downloadID,
TorrentHash: "scan-fail-hash",
},
}
err = worker.Work(ctx, job)
require.NoError(t, err)
var state string
err = suite.pool.QueryRow(ctx, "SELECT state FROM downloads WHERE id = $1", downloadID).Scan(&state)
require.NoError(t, err)
assert.Equal(t, "completed", state)
var fileCount int
err = suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM download_files WHERE download_id = $1", downloadID).Scan(&fileCount)
require.NoError(t, err)
assert.Equal(t, 0, fileCount)
}
func TestRecoverOrphaned_FindsActiveDownloads(t *testing.T) {
suite := setupSuite(t)
cleanTables(t, suite.pool)
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 ('artist-ext-id', '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-id', $1, 'Test Album', 'album', 10, 1, 'Test Label', ARRAY['Rock'], 'http://cover.com', 'monitored')
RETURNING id
`, artistID).Scan(&albumID)
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, 'orphan-hash', 'test-tracker', 'Test Album FLAC', 'FLAC', '16-44', 'CD', 100, 50, 500000000)
RETURNING id
`, albumID).Scan(&torrentID)
require.NoError(t, err)
var downloadID string
err = suite.pool.QueryRow(ctx, `
INSERT INTO downloads (torrent_id, album_id, format, quality, state, qbit_hash)
VALUES ($1, $2, 'FLAC', '16-44', 'downloading', 'orphan-hash')
RETURNING id
`, torrentID, albumID).Scan(&downloadID)
require.NoError(t, err)
downloads := database.NewDownloadRepository(suite.pool)
active, err := downloads.GetActive(ctx)
require.NoError(t, err)
require.Len(t, active, 1)
assert.Equal(t, downloadID, active[0].ID)
assert.Equal(t, "orphan-hash", active[0].QbitHash)
assert.Equal(t, "downloading", active[0].State)
}