Add MonitorAlbum component tests: 21 cases covering all flow diagrams (bufconn + testcontainers + hand-rolled mocks)
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
"homelab.lan/music-agregator/internal/indexer"
|
||||
)
|
||||
|
||||
func newMetadataAlbum(id, title, artistID, artistName string) *metadataPb.Album {
|
||||
return &metadataPb.Album{
|
||||
Id: id,
|
||||
Title: title,
|
||||
AlbumType: "album",
|
||||
ReleaseDate: "2024-01-15",
|
||||
TotalTracks: 10,
|
||||
TotalDiscs: 1,
|
||||
CoverUrl: "https://example.com/cover.jpg",
|
||||
Artists: []*metadataPb.ArtistCredit{
|
||||
{
|
||||
Artist: &metadataPb.Artist{
|
||||
Id: artistID,
|
||||
Name: artistName,
|
||||
},
|
||||
Role: "primary",
|
||||
Position: 1,
|
||||
},
|
||||
},
|
||||
Label: &metadataPb.Label{
|
||||
Id: "label-1",
|
||||
Name: "Test Label",
|
||||
},
|
||||
Genres: []*metadataPb.Genre{
|
||||
{Id: "genre-1", Name: "Rock"},
|
||||
{Id: "genre-2", Name: "Alternative"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newMetadataTrack(id, title string, trackNum int32) *metadataPb.Track {
|
||||
return &metadataPb.Track{
|
||||
Id: id,
|
||||
Title: title,
|
||||
DurationMs: 240000,
|
||||
Isrc: "US-XYZ-24-00001",
|
||||
Explicit: false,
|
||||
DiscNumber: 1,
|
||||
TrackNumber: trackNum,
|
||||
Artists: []*metadataPb.ArtistCredit{
|
||||
{
|
||||
Artist: &metadataPb.Artist{
|
||||
Id: "artist-1",
|
||||
Name: "Test Artist",
|
||||
},
|
||||
Role: "primary",
|
||||
Position: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newSearchResponse(items ...*indexer.SearchItemResult) *indexer.SearchResponse {
|
||||
return &indexer.SearchResponse{
|
||||
Items: items,
|
||||
}
|
||||
}
|
||||
|
||||
func newSearchItem(title string, seeders int, downloadLink string) *indexer.SearchItemResult {
|
||||
return &indexer.SearchItemResult{
|
||||
Title: title,
|
||||
DownloadLink: downloadLink,
|
||||
Size: 500 * 1024 * 1024,
|
||||
Tracker: "test-tracker",
|
||||
Seeders: seeders,
|
||||
Peers: seeders / 2,
|
||||
}
|
||||
}
|
||||
|
||||
func newTorrentData() []byte {
|
||||
return []byte("d8:announce35:http://tracker.example.com/announce4:infod6:lengthi1024e4:name9:test.flac12:piece lengthi16384e6:pieces20:01234567890123456789ee")
|
||||
}
|
||||
|
||||
func newTorrentDataMP3() []byte {
|
||||
return []byte("d8:announce35:http://tracker.example.com/announce4:infod6:lengthi1024e4:name8:test.mp312:piece lengthi16384e6:pieces20:01234567890123456789ee")
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
metadataPb "homelab.lan/music-agregator/gen/metadata/v1"
|
||||
"homelab.lan/music-agregator/internal/indexer"
|
||||
"homelab.lan/music-agregator/internal/torrent"
|
||||
)
|
||||
|
||||
type mockMetadataClient struct {
|
||||
GetAlbumFunc func(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error)
|
||||
GetArtistAlbumsFunc func(ctx context.Context, in *metadataPb.GetArtistAlbumsRequest, opts ...grpc.CallOption) (*metadataPb.GetArtistAlbumsResponse, error)
|
||||
GetAlbumTracksFunc func(ctx context.Context, in *metadataPb.GetAlbumTracksRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumTracksResponse, error)
|
||||
GetArtistFunc func(ctx context.Context, in *metadataPb.GetArtistRequest, opts ...grpc.CallOption) (*metadataPb.GetArtistResponse, error)
|
||||
SearchArtistsFunc func(ctx context.Context, in *metadataPb.SearchArtistsRequest, opts ...grpc.CallOption) (*metadataPb.SearchArtistsResponse, error)
|
||||
GetTrackFunc func(ctx context.Context, in *metadataPb.GetTrackRequest, opts ...grpc.CallOption) (*metadataPb.GetTrackResponse, error)
|
||||
SearchAlbumsFunc func(ctx context.Context, in *metadataPb.SearchAlbumsRequest, opts ...grpc.CallOption) (*metadataPb.SearchAlbumsResponse, error)
|
||||
SyncArtistFunc func(ctx context.Context, in *metadataPb.SyncArtistRequest, opts ...grpc.CallOption) (*metadataPb.SyncArtistResponse, error)
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) GetAlbum(ctx context.Context, in *metadataPb.GetAlbumRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumResponse, error) {
|
||||
if m.GetAlbumFunc != nil {
|
||||
return m.GetAlbumFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) GetArtistAlbums(ctx context.Context, in *metadataPb.GetArtistAlbumsRequest, opts ...grpc.CallOption) (*metadataPb.GetArtistAlbumsResponse, error) {
|
||||
if m.GetArtistAlbumsFunc != nil {
|
||||
return m.GetArtistAlbumsFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) GetAlbumTracks(ctx context.Context, in *metadataPb.GetAlbumTracksRequest, opts ...grpc.CallOption) (*metadataPb.GetAlbumTracksResponse, error) {
|
||||
if m.GetAlbumTracksFunc != nil {
|
||||
return m.GetAlbumTracksFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) GetArtist(ctx context.Context, in *metadataPb.GetArtistRequest, opts ...grpc.CallOption) (*metadataPb.GetArtistResponse, error) {
|
||||
if m.GetArtistFunc != nil {
|
||||
return m.GetArtistFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) SearchArtists(ctx context.Context, in *metadataPb.SearchArtistsRequest, opts ...grpc.CallOption) (*metadataPb.SearchArtistsResponse, error) {
|
||||
if m.SearchArtistsFunc != nil {
|
||||
return m.SearchArtistsFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) GetTrack(ctx context.Context, in *metadataPb.GetTrackRequest, opts ...grpc.CallOption) (*metadataPb.GetTrackResponse, error) {
|
||||
if m.GetTrackFunc != nil {
|
||||
return m.GetTrackFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) SearchAlbums(ctx context.Context, in *metadataPb.SearchAlbumsRequest, opts ...grpc.CallOption) (*metadataPb.SearchAlbumsResponse, error) {
|
||||
if m.SearchAlbumsFunc != nil {
|
||||
return m.SearchAlbumsFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
func (m *mockMetadataClient) SyncArtist(ctx context.Context, in *metadataPb.SyncArtistRequest, opts ...grpc.CallOption) (*metadataPb.SyncArtistResponse, error) {
|
||||
if m.SyncArtistFunc != nil {
|
||||
return m.SyncArtistFunc(ctx, in, opts...)
|
||||
}
|
||||
return nil, status.Error(codes.Unimplemented, "not mocked")
|
||||
}
|
||||
|
||||
type mockTorrentClient struct {
|
||||
LoginFunc func(username, password string) (string, error)
|
||||
ListFunc func() ([]torrent.TorrentInfo, error)
|
||||
FindFunc func(opts torrent.FindOptions) ([]torrent.TorrentInfo, error)
|
||||
AddTorrentFunc func(file torrent.TorrentFile) error
|
||||
AddMagnetFunc func(magnetURI string) error
|
||||
}
|
||||
|
||||
func (m *mockTorrentClient) Login(username, password string) (string, error) {
|
||||
if m.LoginFunc != nil {
|
||||
return m.LoginFunc(username, password)
|
||||
}
|
||||
return "", fmt.Errorf("not mocked")
|
||||
}
|
||||
|
||||
func (m *mockTorrentClient) List() ([]torrent.TorrentInfo, error) {
|
||||
if m.ListFunc != nil {
|
||||
return m.ListFunc()
|
||||
}
|
||||
return nil, fmt.Errorf("not mocked")
|
||||
}
|
||||
|
||||
func (m *mockTorrentClient) Find(opts torrent.FindOptions) ([]torrent.TorrentInfo, error) {
|
||||
if m.FindFunc != nil {
|
||||
return m.FindFunc(opts)
|
||||
}
|
||||
return nil, fmt.Errorf("not mocked")
|
||||
}
|
||||
|
||||
func (m *mockTorrentClient) AddTorrent(file torrent.TorrentFile) error {
|
||||
if m.AddTorrentFunc != nil {
|
||||
return m.AddTorrentFunc(file)
|
||||
}
|
||||
return fmt.Errorf("not mocked")
|
||||
}
|
||||
|
||||
func (m *mockTorrentClient) AddMagnet(magnetURI string) error {
|
||||
if m.AddMagnetFunc != nil {
|
||||
return m.AddMagnetFunc(magnetURI)
|
||||
}
|
||||
return fmt.Errorf("not mocked")
|
||||
}
|
||||
|
||||
type mockSearcher struct {
|
||||
SearchFunc func(query string, limit int32, indexer string) (*indexer.SearchResponse, error)
|
||||
}
|
||||
|
||||
func (m *mockSearcher) Search(query string, limit int32, idx string) (*indexer.SearchResponse, error) {
|
||||
if m.SearchFunc != nil {
|
||||
return m.SearchFunc(query, limit, idx)
|
||||
}
|
||||
return nil, fmt.Errorf("not mocked")
|
||||
}
|
||||
|
||||
type mockResolver struct {
|
||||
ResolveFunc func(magnetURI string) ([]byte, error)
|
||||
}
|
||||
|
||||
func (m *mockResolver) Resolve(magnetURI string) ([]byte, error) {
|
||||
if m.ResolveFunc != nil {
|
||||
return m.ResolveFunc(magnetURI)
|
||||
}
|
||||
return nil, fmt.Errorf("not mocked")
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,174 @@
|
||||
package component
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/postgres"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/test/bufconn"
|
||||
|
||||
pb "homelab.lan/music-agregator/gen/music_agregator/v1"
|
||||
"homelab.lan/music-agregator/internal"
|
||||
"homelab.lan/music-agregator/internal/database"
|
||||
"homelab.lan/music-agregator/internal/metadata"
|
||||
)
|
||||
|
||||
const bufSize = 1024 * 1024
|
||||
|
||||
type testMocks struct {
|
||||
metadata *mockMetadataClient
|
||||
torrent *mockTorrentClient
|
||||
indexer *mockSearcher
|
||||
magnet *mockResolver
|
||||
}
|
||||
|
||||
type testSuite struct {
|
||||
db *database.DB
|
||||
grpcConn *grpc.ClientConn
|
||||
client pb.MusicAgregatorServiceClient
|
||||
pool *pgxpool.Pool
|
||||
mocks *testMocks
|
||||
}
|
||||
|
||||
func setupSuite(t *testing.T) *testSuite {
|
||||
ctx := context.Background()
|
||||
|
||||
schemaPath := getSchemaPath(t)
|
||||
schemaSQL, err := os.ReadFile(schemaPath)
|
||||
require.NoError(t, err, "failed to read schema file")
|
||||
|
||||
pgContainer, err := postgres.Run(ctx,
|
||||
"postgres:16-alpine",
|
||||
postgres.WithDatabase("music_agregator_test"),
|
||||
postgres.WithUsername("test"),
|
||||
postgres.WithPassword("test"),
|
||||
postgres.WithInitScripts(),
|
||||
testcontainers.WithWaitStrategy(
|
||||
wait.ForLog("database system is ready to accept connections").
|
||||
WithOccurrence(2).
|
||||
WithStartupTimeout(30*time.Second),
|
||||
),
|
||||
)
|
||||
require.NoError(t, err, "failed to start postgres container")
|
||||
|
||||
t.Cleanup(func() {
|
||||
if err := pgContainer.Terminate(ctx); err != nil {
|
||||
t.Logf("failed to terminate postgres container: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
|
||||
require.NoError(t, err, "failed to get connection string")
|
||||
|
||||
pool, err := pgxpool.New(ctx, connStr)
|
||||
require.NoError(t, err, "failed to create pgxpool")
|
||||
|
||||
t.Cleanup(func() {
|
||||
pool.Close()
|
||||
})
|
||||
|
||||
_, err = pool.Exec(ctx, string(schemaSQL))
|
||||
require.NoError(t, err, "failed to apply schema")
|
||||
|
||||
db := &database.DB{Pool: pool}
|
||||
|
||||
mocks := &testMocks{
|
||||
metadata: &mockMetadataClient{},
|
||||
torrent: &mockTorrentClient{},
|
||||
indexer: &mockSearcher{},
|
||||
magnet: &mockResolver{},
|
||||
}
|
||||
|
||||
metadataSvc := metadata.NewMetadataService(mocks.metadata, db)
|
||||
|
||||
service := internal.NewMusicAgregatorServiceWithDeps(
|
||||
metadataSvc,
|
||||
mocks.indexer,
|
||||
mocks.torrent,
|
||||
mocks.magnet,
|
||||
nil,
|
||||
db,
|
||||
)
|
||||
|
||||
server := internal.NewMusicAgregatorServerWithService(service)
|
||||
|
||||
lis := bufconn.Listen(bufSize)
|
||||
grpcServer := grpc.NewServer()
|
||||
server.Register(grpcServer)
|
||||
|
||||
go func() {
|
||||
if err := grpcServer.Serve(lis); err != nil {
|
||||
t.Logf("grpc server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
grpcServer.GracefulStop()
|
||||
})
|
||||
|
||||
conn, err := grpc.NewClient(
|
||||
"passthrough://bufnet",
|
||||
grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return lis.DialContext(ctx)
|
||||
}),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
require.NoError(t, err, "failed to create grpc client connection")
|
||||
|
||||
t.Cleanup(func() {
|
||||
conn.Close()
|
||||
})
|
||||
|
||||
client := pb.NewMusicAgregatorServiceClient(conn)
|
||||
|
||||
return &testSuite{
|
||||
db: db,
|
||||
grpcConn: conn,
|
||||
client: client,
|
||||
pool: pool,
|
||||
mocks: mocks,
|
||||
}
|
||||
}
|
||||
|
||||
func getSchemaPath(t *testing.T) string {
|
||||
_, currentFile, _, ok := runtime.Caller(0)
|
||||
require.True(t, ok, "failed to get current file path")
|
||||
|
||||
testDir := filepath.Dir(currentFile)
|
||||
schemaPath := filepath.Join(testDir, "..", "..", "..", "containers", "database", "music-agregator", "002_schema.sql")
|
||||
|
||||
if _, err := os.Stat(schemaPath); os.IsNotExist(err) {
|
||||
schemaPath = filepath.Join(testDir, "..", "..", "containers", "database", "music-agregator", "002_schema.sql")
|
||||
}
|
||||
|
||||
return schemaPath
|
||||
}
|
||||
|
||||
func cleanTables(t *testing.T, pool *pgxpool.Pool) {
|
||||
ctx := context.Background()
|
||||
|
||||
tables := []string{
|
||||
"download_files",
|
||||
"downloads",
|
||||
"torrents",
|
||||
"tracks",
|
||||
"albums",
|
||||
"artists",
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
_, err := pool.Exec(ctx, "TRUNCATE TABLE "+table+" CASCADE")
|
||||
require.NoError(t, err, "failed to truncate table %s", table)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user