package app_test import ( "context" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "sneak.berlin/go/upaas/internal/config" "sneak.berlin/go/upaas/internal/database" "sneak.berlin/go/upaas/internal/globals" "sneak.berlin/go/upaas/internal/logger" "sneak.berlin/go/upaas/internal/models" "sneak.berlin/go/upaas/internal/service/app" ) // backupTestContext bundles test dependencies for backup tests. type backupTestContext struct { svc *app.Service db *database.Database } func setupBackupTest(t *testing.T) *backupTestContext { t.Helper() tmpDir := t.TempDir() globals.SetAppname("upaas-test") globals.SetVersion("test") globalsInst, err := globals.New(fx.Lifecycle(nil)) require.NoError(t, err) loggerInst, err := logger.New( fx.Lifecycle(nil), logger.Params{Globals: globalsInst}, ) require.NoError(t, err) cfg := &config.Config{ Port: 8080, DataDir: tmpDir, SessionSecret: "test-secret-key-at-least-32-chars", } dbInst, err := database.New(fx.Lifecycle(nil), database.Params{ Logger: loggerInst, Config: cfg, }) require.NoError(t, err) svc, err := app.New(fx.Lifecycle(nil), app.ServiceParams{ Logger: loggerInst, Database: dbInst, }) require.NoError(t, err) return &backupTestContext{svc: svc, db: dbInst} } // createAppWithFullConfig creates an app with env vars, labels, volumes, and ports. func createAppWithFullConfig( t *testing.T, btc *backupTestContext, name string, ) *models.App { t.Helper() createdApp, err := btc.svc.CreateApp(context.Background(), app.CreateAppInput{ Name: name, RepoURL: "git@example.com:user/" + name + ".git", Branch: "develop", NtfyTopic: "https://ntfy.sh/" + name, DockerNetwork: "test-network", }) require.NoError(t, err) require.NoError(t, btc.svc.AddEnvVar( context.Background(), createdApp.ID, "DB_HOST", "localhost", )) require.NoError(t, btc.svc.AddEnvVar( context.Background(), createdApp.ID, "DB_PORT", "5432", )) require.NoError(t, btc.svc.AddLabel( context.Background(), createdApp.ID, "traefik.enable", "true", )) require.NoError(t, btc.svc.AddVolume( context.Background(), createdApp.ID, "/data", "/app/data", false, )) port := models.NewPort(btc.db) port.AppID = createdApp.ID port.HostPort = 9090 port.ContainerPort = 8080 port.Protocol = models.PortProtocolTCP require.NoError(t, port.Save(context.Background())) return createdApp } // createAppWithConfigPort creates an app like createAppWithFullConfig but with // a custom host port to avoid UNIQUE constraint collisions. func createAppWithConfigPort( t *testing.T, btc *backupTestContext, name string, hostPort int, ) *models.App { t.Helper() createdApp, err := btc.svc.CreateApp(context.Background(), app.CreateAppInput{ Name: name, RepoURL: "git@example.com:user/" + name + ".git", Branch: "develop", NtfyTopic: "https://ntfy.sh/" + name, DockerNetwork: "test-network", }) require.NoError(t, err) require.NoError(t, btc.svc.AddEnvVar( context.Background(), createdApp.ID, "DB_HOST", "localhost", )) require.NoError(t, btc.svc.AddLabel( context.Background(), createdApp.ID, "traefik.enable", "true", )) require.NoError(t, btc.svc.AddVolume( context.Background(), createdApp.ID, "/data2", "/app/data2", false, )) port := models.NewPort(btc.db) port.AppID = createdApp.ID port.HostPort = hostPort port.ContainerPort = 8080 port.Protocol = models.PortProtocolTCP require.NoError(t, port.Save(context.Background())) return createdApp } func TestExportApp(t *testing.T) { t.Parallel() btc := setupBackupTest(t) createdApp := createAppWithFullConfig(t, btc, "export-svc-test") bundle, err := btc.svc.ExportApp(context.Background(), createdApp) require.NoError(t, err) assert.Equal(t, 1, bundle.Version) assert.NotEmpty(t, bundle.ExportedAt) require.Len(t, bundle.Apps, 1) ab := bundle.Apps[0] assert.Equal(t, "export-svc-test", ab.Name) assert.Equal(t, "develop", ab.Branch) assert.Equal(t, "test-network", ab.DockerNetwork) assert.Equal(t, "https://ntfy.sh/export-svc-test", ab.NtfyTopic) assert.Len(t, ab.EnvVars, 2) assert.Len(t, ab.Labels, 1) assert.Len(t, ab.Volumes, 1) assert.Len(t, ab.Ports, 1) assert.Equal(t, 9090, ab.Ports[0].HostPort) assert.Equal(t, 8080, ab.Ports[0].ContainerPort) assert.Equal(t, "tcp", ab.Ports[0].Protocol) } func TestExportAllApps(t *testing.T) { t.Parallel() btc := setupBackupTest(t) createAppWithFullConfig(t, btc, "export-all-1") createAppWithConfigPort(t, btc, "export-all-2", 9091) bundle, err := btc.svc.ExportAllApps(context.Background()) require.NoError(t, err) assert.Equal(t, 1, bundle.Version) assert.Len(t, bundle.Apps, 2) } func TestExportAllAppsEmpty(t *testing.T) { t.Parallel() btc := setupBackupTest(t) bundle, err := btc.svc.ExportAllApps(context.Background()) require.NoError(t, err) assert.Empty(t, bundle.Apps) } func TestImportApps(t *testing.T) { t.Parallel() btc := setupBackupTest(t) bundle := &app.BackupBundle{ Version: 1, ExportedAt: "2025-01-01T00:00:00Z", Apps: []app.Backup{ { Name: "imported-test", RepoURL: "git@example.com:user/imported.git", Branch: "main", DockerfilePath: "Dockerfile", DockerNetwork: "my-network", EnvVars: []app.BackupEnvVar{ {Key: "FOO", Value: "bar"}, }, Labels: []app.BackupLabel{ {Key: "app", Value: "test"}, }, Volumes: []app.BackupVolume{ {HostPath: "/host", ContainerPath: "/container", ReadOnly: true}, }, Ports: []app.BackupPort{ {HostPort: 3000, ContainerPort: 8080, Protocol: "tcp"}, }, }, }, } imported, skipped, err := btc.svc.ImportApps(context.Background(), bundle) require.NoError(t, err) assert.Equal(t, []string{"imported-test"}, imported) assert.Empty(t, skipped) // Verify the app was created apps, _ := btc.svc.ListApps(context.Background()) require.Len(t, apps, 1) assert.Equal(t, "imported-test", apps[0].Name) assert.True(t, apps[0].DockerNetwork.Valid) assert.Equal(t, "my-network", apps[0].DockerNetwork.String) // Has fresh secrets assert.NotEmpty(t, apps[0].WebhookSecret) assert.NotEmpty(t, apps[0].SSHPublicKey) // Verify sub-resources envVars, _ := apps[0].GetEnvVars(context.Background()) assert.Len(t, envVars, 1) labels, _ := apps[0].GetLabels(context.Background()) assert.Len(t, labels, 1) volumes, _ := apps[0].GetVolumes(context.Background()) assert.Len(t, volumes, 1) assert.True(t, volumes[0].ReadOnly) ports, _ := apps[0].GetPorts(context.Background()) assert.Len(t, ports, 1) assert.Equal(t, 3000, ports[0].HostPort) } func TestImportAppsSkipsDuplicates(t *testing.T) { t.Parallel() btc := setupBackupTest(t) // Create existing app _, err := btc.svc.CreateApp(context.Background(), app.CreateAppInput{ Name: "existing", RepoURL: "git@example.com:user/existing.git", }) require.NoError(t, err) bundle := &app.BackupBundle{ Version: 1, ExportedAt: "2025-01-01T00:00:00Z", Apps: []app.Backup{ { Name: "existing", RepoURL: "git@example.com:user/existing.git", Branch: "main", DockerfilePath: "Dockerfile", EnvVars: []app.BackupEnvVar{}, Labels: []app.BackupLabel{}, Volumes: []app.BackupVolume{}, Ports: []app.BackupPort{}, }, { Name: "brand-new", RepoURL: "git@example.com:user/new.git", Branch: "main", DockerfilePath: "Dockerfile", EnvVars: []app.BackupEnvVar{}, Labels: []app.BackupLabel{}, Volumes: []app.BackupVolume{}, Ports: []app.BackupPort{}, }, }, } imported, skipped, err := btc.svc.ImportApps(context.Background(), bundle) require.NoError(t, err) assert.Equal(t, []string{"brand-new"}, imported) assert.Equal(t, []string{"existing"}, skipped) } func TestImportAppsPortDefaultProtocol(t *testing.T) { t.Parallel() btc := setupBackupTest(t) bundle := &app.BackupBundle{ Version: 1, ExportedAt: "2025-01-01T00:00:00Z", Apps: []app.Backup{ { Name: "port-default-test", RepoURL: "git@example.com:user/repo.git", Branch: "main", DockerfilePath: "Dockerfile", EnvVars: []app.BackupEnvVar{}, Labels: []app.BackupLabel{}, Volumes: []app.BackupVolume{}, Ports: []app.BackupPort{ {HostPort: 80, ContainerPort: 80, Protocol: ""}, }, }, }, } imported, _, err := btc.svc.ImportApps(context.Background(), bundle) require.NoError(t, err) assert.Len(t, imported, 1) apps, _ := btc.svc.ListApps(context.Background()) ports, _ := apps[0].GetPorts(context.Background()) require.Len(t, ports, 1) assert.Equal(t, models.PortProtocolTCP, ports[0].Protocol) } func TestExportImportRoundTripService(t *testing.T) { t.Parallel() btc := setupBackupTest(t) createAppWithFullConfig(t, btc, "roundtrip-svc") // Export bundle, err := btc.svc.ExportAllApps(context.Background()) require.NoError(t, err) require.Len(t, bundle.Apps, 1) // Delete original apps, _ := btc.svc.ListApps(context.Background()) for _, a := range apps { require.NoError(t, btc.svc.DeleteApp(context.Background(), a)) } // Import imported, skipped, err := btc.svc.ImportApps(context.Background(), bundle) require.NoError(t, err) assert.Len(t, imported, 1) assert.Empty(t, skipped) // Verify round-trip fidelity restored, _ := btc.svc.ListApps(context.Background()) require.Len(t, restored, 1) assert.Equal(t, "roundtrip-svc", restored[0].Name) assert.Equal(t, "develop", restored[0].Branch) assert.Equal(t, "test-network", restored[0].DockerNetwork.String) envVars, _ := restored[0].GetEnvVars(context.Background()) assert.Len(t, envVars, 2) labels, _ := restored[0].GetLabels(context.Background()) assert.Len(t, labels, 1) volumes, _ := restored[0].GetVolumes(context.Background()) assert.Len(t, volumes, 1) ports, _ := restored[0].GetPorts(context.Background()) assert.Len(t, ports, 1) }