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") migrationPath := getMigrationPath(t, "003_event_bus.sql") migrationSQL, err := os.ReadFile(migrationPath) require.NoError(t, err, "failed to read migration 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") _, err = pool.Exec(ctx, string(migrationSQL)) require.NoError(t, err, "failed to apply migration") 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, 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 getMigrationPath(t *testing.T, filename string) string { _, currentFile, _, ok := runtime.Caller(0) require.True(t, ok, "failed to get current file path") testDir := filepath.Dir(currentFile) migrationPath := filepath.Join(testDir, "..", "..", "..", "containers", "database", "music-agregator", filename) if _, err := os.Stat(migrationPath); os.IsNotExist(err) { migrationPath = filepath.Join(testDir, "..", "..", "containers", "database", "music-agregator", filename) } return migrationPath } func cleanTables(t *testing.T, pool *pgxpool.Pool) { ctx := context.Background() tables := []string{ "album_events", "workflow_runs", "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) } }