24f355c5ae
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1528 lines
47 KiB
Go
1528 lines
47 KiB
Go
package component
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"testing"
|
|
"time"
|
|
|
|
"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"
|
|
"homelab.lan/music-agregator/internal/indexer"
|
|
"homelab.lan/music-agregator/internal/torrent"
|
|
)
|
|
|
|
const defaultStreamTimeout = 5 * time.Second
|
|
|
|
func startMonitorStream(
|
|
t *testing.T,
|
|
client pb.MusicAgregatorServiceClient,
|
|
req *pb.StartMonitorRequest,
|
|
) grpc.BidiStreamingClient[pb.MonitorAlbumStreamRequest, pb.MonitorAlbumStreamResponse] {
|
|
t.Helper()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
t.Cleanup(cancel)
|
|
|
|
stream, err := client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{Start: req},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return stream
|
|
}
|
|
|
|
func collectAllMessages(
|
|
t *testing.T,
|
|
stream grpc.BidiStreamingClient[pb.MonitorAlbumStreamRequest, pb.MonitorAlbumStreamResponse],
|
|
timeout time.Duration,
|
|
) []*pb.MonitorAlbumStreamResponse {
|
|
t.Helper()
|
|
|
|
if timeout == 0 {
|
|
timeout = defaultStreamTimeout
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
var messages []*pb.MonitorAlbumStreamResponse
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(done)
|
|
for {
|
|
msg, err := stream.Recv()
|
|
if err == io.EOF {
|
|
return
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
messages = append(messages, msg)
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-ctx.Done():
|
|
t.Fatalf("collectAllMessages timed out after %v", timeout)
|
|
}
|
|
|
|
return messages
|
|
}
|
|
|
|
func collectUntilPrompt(
|
|
t *testing.T,
|
|
stream grpc.BidiStreamingClient[pb.MonitorAlbumStreamRequest, pb.MonitorAlbumStreamResponse],
|
|
timeout time.Duration,
|
|
) ([]*pb.MonitorAlbumStreamResponse, *pb.PromptForDecision, error) {
|
|
t.Helper()
|
|
|
|
if timeout == 0 {
|
|
timeout = defaultStreamTimeout
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
var messages []*pb.MonitorAlbumStreamResponse
|
|
resultChan := make(chan struct {
|
|
prompt *pb.PromptForDecision
|
|
err error
|
|
}, 1)
|
|
|
|
go func() {
|
|
for {
|
|
msg, err := stream.Recv()
|
|
if err == io.EOF {
|
|
resultChan <- struct {
|
|
prompt *pb.PromptForDecision
|
|
err error
|
|
}{nil, io.EOF}
|
|
return
|
|
}
|
|
if err != nil {
|
|
resultChan <- struct {
|
|
prompt *pb.PromptForDecision
|
|
err error
|
|
}{nil, err}
|
|
return
|
|
}
|
|
messages = append(messages, msg)
|
|
if prompt := msg.GetPrompt(); prompt != nil {
|
|
resultChan <- struct {
|
|
prompt *pb.PromptForDecision
|
|
err error
|
|
}{prompt, nil}
|
|
return
|
|
}
|
|
if errUpdate := msg.GetError(); errUpdate != nil {
|
|
resultChan <- struct {
|
|
prompt *pb.PromptForDecision
|
|
err error
|
|
}{nil, status.Error(codes.Internal, errUpdate.Message)}
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
select {
|
|
case result := <-resultChan:
|
|
return messages, result.prompt, result.err
|
|
case <-ctx.Done():
|
|
t.Fatalf("collectUntilPrompt timed out after %v", timeout)
|
|
return nil, nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
func sendDecision(
|
|
t *testing.T,
|
|
stream grpc.BidiStreamingClient[pb.MonitorAlbumStreamRequest, pb.MonitorAlbumStreamResponse],
|
|
promptID string,
|
|
decision *pb.UserDecision,
|
|
) {
|
|
t.Helper()
|
|
decision.PromptId = promptID
|
|
err := stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Decision{Decision: decision},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func assertStatusSequence(t *testing.T, messages []*pb.MonitorAlbumStreamResponse, expectedSteps []pb.MonitorStep) {
|
|
t.Helper()
|
|
|
|
var actualSteps []pb.MonitorStep
|
|
for _, msg := range messages {
|
|
if status := msg.GetStatus(); status != nil {
|
|
actualSteps = append(actualSteps, status.Step)
|
|
}
|
|
}
|
|
|
|
assert.Equal(t, expectedSteps, actualSteps, "status step sequence mismatch")
|
|
}
|
|
|
|
func assertContainsStep(t *testing.T, messages []*pb.MonitorAlbumStreamResponse, step pb.MonitorStep) {
|
|
t.Helper()
|
|
|
|
for _, msg := range messages {
|
|
if status := msg.GetStatus(); status != nil && status.Step == step {
|
|
return
|
|
}
|
|
}
|
|
t.Errorf("expected messages to contain step %v", step)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticHappyPath(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
assertStatusSequence(t, messages, []pb.MonitorStep{
|
|
pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA,
|
|
pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA,
|
|
pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED,
|
|
pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER,
|
|
pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS,
|
|
pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY,
|
|
pb.MonitorStep_MONITOR_STEP_SELECTING_RELEASE,
|
|
pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT,
|
|
pb.MonitorStep_MONITOR_STEP_SAVING,
|
|
pb.MonitorStep_MONITOR_STEP_COMPLETE,
|
|
})
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Album)
|
|
assert.Equal(t, "Test Album", result.Album.Title)
|
|
require.NotNil(t, result.Release)
|
|
|
|
ctx := context.Background()
|
|
var torrentCount int
|
|
err := suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&torrentCount)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, torrentCount)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticMetadataUnavailable(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")
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var hasError bool
|
|
for _, msg := range messages {
|
|
if errUpdate := msg.GetError(); errUpdate != nil {
|
|
hasError = true
|
|
assert.Equal(t, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, errUpdate.FailedStep)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasError, "expected error message for metadata unavailable")
|
|
|
|
ctx := context.Background()
|
|
var count int
|
|
err := suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM artists").Scan(&count)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticMetadataNotFound(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")
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "nonexistent-album",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var hasError bool
|
|
for _, msg := range messages {
|
|
if errUpdate := msg.GetError(); errUpdate != nil {
|
|
hasError = true
|
|
assert.Equal(t, pb.MonitorStep_MONITOR_STEP_FETCHING_METADATA, errUpdate.FailedStep)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasError, "expected error message for metadata not found")
|
|
|
|
ctx := context.Background()
|
|
var count int
|
|
err := suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM albums").Scan(&count)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticArtistPersistFails(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "orphan-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Release)
|
|
assert.Nil(t, result.Album)
|
|
|
|
ctx := context.Background()
|
|
var count int
|
|
err := suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&count)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticAlreadyOwned(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "owned-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
assertContainsStep(t, messages, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Album)
|
|
assert.Equal(t, pb.MonitorState_MONITOR_STATE_MONITORED, result.Album.MonitorState)
|
|
assert.Nil(t, result.Release)
|
|
assert.False(t, indexerCalled)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticIndexerDown(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var hasError bool
|
|
for _, msg := range messages {
|
|
if errUpdate := msg.GetError(); errUpdate != nil {
|
|
hasError = true
|
|
assert.Equal(t, pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER, errUpdate.FailedStep)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasError, "expected error message for indexer down")
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticNoResults(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
assertContainsStep(t, messages, pb.MonitorStep_MONITOR_STEP_COMPLETE)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Album)
|
|
assert.Nil(t, result.Release)
|
|
|
|
ctx := context.Background()
|
|
var count int
|
|
err := suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM torrents").Scan(&count)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, count)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticAllSeedersZero(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Album)
|
|
assert.Nil(t, result.Release)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticAllMagnetsFail(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Album)
|
|
assert.Nil(t, result.Release)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticNoQualityMatch(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
assertContainsStep(t, messages, pb.MonitorStep_MONITOR_STEP_FILTERING_QUALITY)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Album)
|
|
assert.Nil(t, result.Release)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticQBitDown(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var hasError bool
|
|
for _, msg := range messages {
|
|
if errUpdate := msg.GetError(); errUpdate != nil {
|
|
hasError = true
|
|
assert.Equal(t, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT, errUpdate.FailedStep)
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasError, "expected error message for qbit down")
|
|
|
|
ctx := context.Background()
|
|
var downloadCount int
|
|
err := suite.pool.QueryRow(ctx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, downloadCount)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_AutomaticTorrentExists(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.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 TestMonitorAlbumStream_AutomaticDuplicateSkipped(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
|
|
}
|
|
|
|
stream := startMonitorStream(t, suite.client, &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
})
|
|
|
|
messages := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range messages {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.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 TestMonitorAlbumStream_ManualSelectTorrents(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 24/96", 50, "magnet:?xt=urn:btih:hash1"),
|
|
newSearchItem("Test Artist - Test Album FLAC 16/44", 100, "magnet:?xt=urn:btih:hash2"),
|
|
newSearchItem("Test Artist - Test Album MP3", 200, "magnet:?xt=urn:btih:hash3"),
|
|
), 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, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
messages, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_SELECT_MANY, prompt.Type)
|
|
require.NotNil(t, prompt.GetSelectMany())
|
|
assert.GreaterOrEqual(t, len(prompt.GetSelectMany().Options), 2)
|
|
|
|
assertContainsStep(t, messages, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS)
|
|
|
|
sendDecision(t, stream, prompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_SelectedIds{
|
|
SelectedIds: &pb.SelectedIds{
|
|
Ids: []string{prompt.GetSelectMany().Options[0].Id, prompt.GetSelectMany().Options[1].Id},
|
|
},
|
|
},
|
|
})
|
|
|
|
_, selectOnePrompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, selectOnePrompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_SELECT_ONE, selectOnePrompt.Type)
|
|
|
|
sendDecision(t, stream, selectOnePrompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_SelectedId{SelectedId: selectOnePrompt.GetSelectOne().DefaultId},
|
|
})
|
|
|
|
_, confirmPrompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, confirmPrompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_CONFIRM, confirmPrompt.Type)
|
|
|
|
sendDecision(t, stream, confirmPrompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
})
|
|
|
|
remaining := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range remaining {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_ManualSelectRelease(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 24/96", 50, "magnet:?xt=urn:btih:hash1"),
|
|
newSearchItem("Test Artist - Test Album FLAC 16/44", 100, "magnet:?xt=urn:btih:hash2"),
|
|
), 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, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
messages, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_SELECT_MANY, prompt.Type)
|
|
require.NotNil(t, prompt.GetSelectMany())
|
|
assert.Len(t, prompt.GetSelectMany().Options, 2)
|
|
|
|
assertContainsStep(t, messages, pb.MonitorStep_MONITOR_STEP_PARSING_RESULTS)
|
|
|
|
sendDecision(t, stream, prompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_SelectedIds{SelectedIds: &pb.SelectedIds{Ids: prompt.GetSelectMany().DefaultIds}},
|
|
})
|
|
|
|
_, selectOnePrompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, selectOnePrompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_SELECT_ONE, selectOnePrompt.Type)
|
|
require.NotNil(t, selectOnePrompt.GetSelectOne())
|
|
assert.Len(t, selectOnePrompt.GetSelectOne().Options, 2)
|
|
|
|
selectedOption := selectOnePrompt.GetSelectOne().Options[1]
|
|
sendDecision(t, stream, selectOnePrompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_SelectedId{SelectedId: selectedOption.Id},
|
|
})
|
|
|
|
_, confirmPrompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, confirmPrompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_CONFIRM, confirmPrompt.Type)
|
|
|
|
sendDecision(t, stream, confirmPrompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
})
|
|
|
|
remaining := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range remaining {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Release)
|
|
assert.Equal(t, int32(100), result.Release.Seeders)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_ManualConfirmAdd(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"),
|
|
), 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
|
|
}
|
|
|
|
var addMagnetCalled bool
|
|
suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error {
|
|
addMagnetCalled = true
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_CONFIRM, prompt.Type)
|
|
|
|
sendDecision(t, stream, prompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
})
|
|
|
|
remaining := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range remaining {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
require.NotNil(t, result.Release)
|
|
assert.True(t, addMagnetCalled)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_ManualRejectAdd(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"),
|
|
), 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
|
|
}
|
|
|
|
var addMagnetCalled bool
|
|
suite.mocks.torrent.AddMagnetFunc = func(magnetURI string, savePath string) error {
|
|
addMagnetCalled = true
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_CONFIRM, prompt.Type)
|
|
|
|
sendDecision(t, stream, prompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_Confirm{Confirm: false},
|
|
})
|
|
|
|
remaining := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range remaining {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
assert.Nil(t, result.Release)
|
|
assert.False(t, addMagnetCalled)
|
|
|
|
bgCtx := context.Background()
|
|
var downloadCount int
|
|
err = suite.pool.QueryRow(bgCtx, "SELECT COUNT(*) FROM downloads").Scan(&downloadCount)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, downloadCount)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_ManualAlreadyOwnedContinue(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
|
|
}
|
|
|
|
suite.mocks.indexer.SearchFunc = func(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
|
|
return newSearchResponse(
|
|
newSearchItem("Owned Album [FLAC 24/96]", 50, "magnet:?xt=urn:btih:newhash"),
|
|
), 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
|
|
}
|
|
|
|
streamCtx, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(streamCtx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "owned-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
messages, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_CONFIRM, prompt.Type)
|
|
|
|
assertContainsStep(t, messages, pb.MonitorStep_MONITOR_STEP_CHECKING_OWNED)
|
|
|
|
sendDecision(t, stream, prompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
})
|
|
|
|
addMessages, addPrompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, addPrompt)
|
|
assert.Equal(t, pb.PromptType_PROMPT_TYPE_CONFIRM, addPrompt.Type)
|
|
|
|
assertContainsStep(t, addMessages, pb.MonitorStep_MONITOR_STEP_SEARCHING_INDEXER)
|
|
assertContainsStep(t, addMessages, pb.MonitorStep_MONITOR_STEP_ADDING_TORRENT)
|
|
|
|
sendDecision(t, stream, addPrompt.PromptId, &pb.UserDecision{
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
})
|
|
|
|
remaining := collectAllMessages(t, stream, 0)
|
|
|
|
var result *pb.MonitorAlbumResponse
|
|
for _, msg := range remaining {
|
|
if r := msg.GetResult(); r != nil {
|
|
result = r
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, result)
|
|
}
|
|
|
|
func TestMonitorAlbumStream_FirstMessageMustBeStart(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Decision{
|
|
Decision: &pb.UserDecision{
|
|
PromptId: "some-prompt-id",
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = stream.Recv()
|
|
require.Error(t, err)
|
|
|
|
st, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.InvalidArgument, st.Code())
|
|
}
|
|
|
|
func TestMonitorAlbumStream_ContextCancellation(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) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(2 * time.Second):
|
|
return &metadataPb.GetAlbumResponse{
|
|
Album: newMetadataAlbum("test-album-ext-id", "Test Album", "artist-ext-id", "Test Artist"),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_AUTOMATIC,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var lastErr error
|
|
for {
|
|
_, err = stream.Recv()
|
|
if err != nil {
|
|
lastErr = err
|
|
break
|
|
}
|
|
}
|
|
require.Error(t, lastErr)
|
|
|
|
st, ok := status.FromError(lastErr)
|
|
if ok {
|
|
assert.Contains(t, []codes.Code{codes.Canceled, codes.DeadlineExceeded}, st.Code())
|
|
}
|
|
}
|
|
|
|
func TestMonitorAlbumStream_PromptTimeout(t *testing.T) {
|
|
suite := setupSuite(t)
|
|
cleanTables(t, suite.pool)
|
|
|
|
origTimeout := internal.MaxPromptTimeout
|
|
internal.MaxPromptTimeout = 3 * time.Second
|
|
t.Cleanup(func() { internal.MaxPromptTimeout = origTimeout })
|
|
|
|
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"),
|
|
), 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, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
|
|
remaining := collectAllMessages(t, stream, 15*time.Second)
|
|
|
|
var hasResult bool
|
|
for _, msg := range remaining {
|
|
if r := msg.GetResult(); r != nil {
|
|
hasResult = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasResult, "expected workflow to complete with default decision after timeout")
|
|
}
|
|
|
|
func TestMonitorAlbumStream_InvalidPromptId(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"),
|
|
), 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, cancel := context.WithTimeout(context.Background(), defaultStreamTimeout)
|
|
defer cancel()
|
|
|
|
stream, err := suite.client.MonitorAlbumStream(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Start{
|
|
Start: &pb.StartMonitorRequest{
|
|
AlbumId: "test-album-ext-id",
|
|
Quality: pb.QualityType_QUALITY_LOSSLESS,
|
|
Mode: pb.InteractionMode_INTERACTION_MODE_MANUAL,
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, prompt, err := collectUntilPrompt(t, stream, 0)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, prompt)
|
|
|
|
err = stream.Send(&pb.MonitorAlbumStreamRequest{
|
|
Message: &pb.MonitorAlbumStreamRequest_Decision{
|
|
Decision: &pb.UserDecision{
|
|
PromptId: "wrong-prompt-id",
|
|
Decision: &pb.UserDecision_Confirm{Confirm: true},
|
|
},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remaining := collectAllMessages(t, stream, 0)
|
|
|
|
var hasError bool
|
|
for _, msg := range remaining {
|
|
if errUpdate := msg.GetError(); errUpdate != nil {
|
|
hasError = true
|
|
assert.Contains(t, errUpdate.Message, "prompt")
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, hasError, "expected error for invalid prompt ID")
|
|
}
|