package models_test import ( "context" "database/sql" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "git.eeqj.de/sneak/upaas/internal/config" "git.eeqj.de/sneak/upaas/internal/database" "git.eeqj.de/sneak/upaas/internal/globals" "git.eeqj.de/sneak/upaas/internal/logger" "git.eeqj.de/sneak/upaas/internal/models" ) // Test constants to satisfy goconst linter. const ( testHash = "hash" testBranch = "main" testValue = "value" testEventType = "push" ) func setupTestDB(t *testing.T) (*database.Database, func()) { t.Helper() tmpDir := t.TempDir() globals.SetAppname("upaas-test") globals.SetVersion("test") globalVars, err := globals.New(fx.Lifecycle(nil)) require.NoError(t, err) logr, err := logger.New(fx.Lifecycle(nil), logger.Params{ Globals: globalVars, }) require.NoError(t, err) cfg := &config.Config{ Port: 8080, DataDir: tmpDir, SessionSecret: "test-secret-key-at-least-32-chars", } testDB, err := database.New(fx.Lifecycle(nil), database.Params{ Logger: logr, Config: cfg, }) require.NoError(t, err) // t.TempDir() automatically cleans up after test cleanup := func() {} return testDB, cleanup } // User Tests. func TestUserCreateAndFind(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() user := models.NewUser(testDB) user.Username = "testuser" user.PasswordHash = "hashed_password" err := user.Save(context.Background()) require.NoError(t, err) assert.NotZero(t, user.ID) assert.NotZero(t, user.CreatedAt) found, err := models.FindUser(context.Background(), testDB, user.ID) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, "testuser", found.Username) } func TestUserUpdate(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() user := models.NewUser(testDB) user.Username = "original" user.PasswordHash = "hash1" err := user.Save(context.Background()) require.NoError(t, err) user.Username = "updated" user.PasswordHash = "hash2" err = user.Save(context.Background()) require.NoError(t, err) found, err := models.FindUser(context.Background(), testDB, user.ID) require.NoError(t, err) assert.Equal(t, "updated", found.Username) assert.Equal(t, "hash2", found.PasswordHash) } func TestUserDelete(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() user := models.NewUser(testDB) user.Username = "todelete" user.PasswordHash = testHash err := user.Save(context.Background()) require.NoError(t, err) err = user.Delete(context.Background()) require.NoError(t, err) found, err := models.FindUser(context.Background(), testDB, user.ID) require.NoError(t, err) assert.Nil(t, found) } func TestUserFindByUsername(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() user := models.NewUser(testDB) user.Username = "findme" user.PasswordHash = testHash err := user.Save(context.Background()) require.NoError(t, err) found, err := models.FindUserByUsername( context.Background(), testDB, "findme", ) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, user.ID, found.ID) } func TestUserFindByUsernameNotFound(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() found, err := models.FindUserByUsername( context.Background(), testDB, "nonexistent", ) require.NoError(t, err) assert.Nil(t, found) } func TestUserExists(t *testing.T) { t.Parallel() t.Run("returns false when no users", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() exists, err := models.UserExists(context.Background(), testDB) require.NoError(t, err) assert.False(t, exists) }) t.Run("returns true when user exists", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() user := models.NewUser(testDB) user.Username = "admin" user.PasswordHash = testHash err := user.Save(context.Background()) require.NoError(t, err) exists, err := models.UserExists(context.Background(), testDB) require.NoError(t, err) assert.True(t, exists) }) } // App Tests. func TestAppCreateAndFind(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) assert.NotZero(t, app.CreatedAt) found, err := models.FindApp(context.Background(), testDB, app.ID) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, "test-app", found.Name) assert.Equal(t, models.AppStatusPending, found.Status) } func TestAppUpdate(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) app.Name = "updated" app.Status = models.AppStatusRunning app.ContainerID = sql.NullString{String: "container123", Valid: true} err := app.Save(context.Background()) require.NoError(t, err) found, err := models.FindApp(context.Background(), testDB, app.ID) require.NoError(t, err) assert.Equal(t, "updated", found.Name) assert.Equal(t, models.AppStatusRunning, found.Status) assert.Equal(t, "container123", found.ContainerID.String) } func TestAppDelete(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) err := app.Delete(context.Background()) require.NoError(t, err) found, err := models.FindApp(context.Background(), testDB, app.ID) require.NoError(t, err) assert.Nil(t, found) } func TestAppFindByWebhookSecret(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) found, err := models.FindAppByWebhookSecret( context.Background(), testDB, app.WebhookSecret, ) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, app.ID, found.ID) } func TestAllApps(t *testing.T) { t.Parallel() t.Run("returns empty list when no apps", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() apps, err := models.AllApps(context.Background(), testDB) require.NoError(t, err) assert.Empty(t, apps) }) t.Run("returns apps ordered by name", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() names := []string{"zebra", "alpha", "mike"} for idx, name := range names { app := models.NewApp(testDB) app.ID = name + "-id" app.Name = name app.RepoURL = "git@example.com:user/" + name + ".git" app.Branch = testBranch app.DockerfilePath = "Dockerfile" app.WebhookSecret = "secret-" + strconv.Itoa(idx) app.SSHPrivateKey = "private" app.SSHPublicKey = "public" err := app.Save(context.Background()) require.NoError(t, err) } apps, err := models.AllApps(context.Background(), testDB) require.NoError(t, err) require.Len(t, apps, 3) assert.Equal(t, "alpha", apps[0].Name) assert.Equal(t, "mike", apps[1].Name) assert.Equal(t, "zebra", apps[2].Name) }) } // EnvVar Tests. func TestEnvVarCRUD(t *testing.T) { t.Parallel() t.Run("creates and finds env vars", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() // Create app first. app := createTestApp(t, testDB) envVar := models.NewEnvVar(testDB) envVar.AppID = app.ID envVar.Key = "DATABASE_URL" envVar.Value = "postgres://localhost/db" err := envVar.Save(context.Background()) require.NoError(t, err) assert.NotZero(t, envVar.ID) envVars, err := models.FindEnvVarsByAppID( context.Background(), testDB, app.ID, ) require.NoError(t, err) require.Len(t, envVars, 1) assert.Equal(t, "DATABASE_URL", envVars[0].Key) }) t.Run("deletes env var", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) envVar := models.NewEnvVar(testDB) envVar.AppID = app.ID envVar.Key = "TO_DELETE" envVar.Value = testValue err := envVar.Save(context.Background()) require.NoError(t, err) err = envVar.Delete(context.Background()) require.NoError(t, err) envVars, err := models.FindEnvVarsByAppID( context.Background(), testDB, app.ID, ) require.NoError(t, err) assert.Empty(t, envVars) }) } // Label Tests. func TestLabelCRUD(t *testing.T) { t.Parallel() t.Run("creates and finds labels", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) label := models.NewLabel(testDB) label.AppID = app.ID label.Key = "traefik.enable" label.Value = "true" err := label.Save(context.Background()) require.NoError(t, err) assert.NotZero(t, label.ID) labels, err := models.FindLabelsByAppID( context.Background(), testDB, app.ID, ) require.NoError(t, err) require.Len(t, labels, 1) assert.Equal(t, "traefik.enable", labels[0].Key) }) } // Volume Tests. func TestVolumeCRUD(t *testing.T) { t.Parallel() t.Run("creates and finds volumes", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) volume := models.NewVolume(testDB) volume.AppID = app.ID volume.HostPath = "/data/app" volume.ContainerPath = "/app/data" volume.ReadOnly = true err := volume.Save(context.Background()) require.NoError(t, err) assert.NotZero(t, volume.ID) volumes, err := models.FindVolumesByAppID( context.Background(), testDB, app.ID, ) require.NoError(t, err) require.Len(t, volumes, 1) assert.Equal(t, "/data/app", volumes[0].HostPath) assert.True(t, volumes[0].ReadOnly) }) } // WebhookEvent Tests. func TestWebhookEventCRUD(t *testing.T) { t.Parallel() t.Run("creates and finds webhook events", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) event := models.NewWebhookEvent(testDB) event.AppID = app.ID event.EventType = testEventType event.Branch = testBranch event.CommitSHA = sql.NullString{String: "abc123", Valid: true} event.Matched = true err := event.Save(context.Background()) require.NoError(t, err) assert.NotZero(t, event.ID) events, err := models.FindWebhookEventsByAppID( context.Background(), testDB, app.ID, 10, ) require.NoError(t, err) require.Len(t, events, 1) assert.Equal(t, "push", events[0].EventType) assert.True(t, events[0].Matched) }) } // Deployment Tests. func TestDeploymentCreateAndFind(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) deployment := models.NewDeployment(testDB) deployment.AppID = app.ID deployment.CommitSHA = sql.NullString{String: "abc123def456", Valid: true} deployment.Status = models.DeploymentStatusBuilding err := deployment.Save(context.Background()) require.NoError(t, err) assert.NotZero(t, deployment.ID) assert.NotZero(t, deployment.StartedAt) found, err := models.FindDeployment(context.Background(), testDB, deployment.ID) require.NoError(t, err) require.NotNil(t, found) assert.Equal(t, models.DeploymentStatusBuilding, found.Status) } func TestDeploymentAppendLog(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) deployment := models.NewDeployment(testDB) deployment.AppID = app.ID deployment.Status = models.DeploymentStatusBuilding err := deployment.Save(context.Background()) require.NoError(t, err) err = deployment.AppendLog(context.Background(), "Building image...") require.NoError(t, err) err = deployment.AppendLog(context.Background(), "Image built successfully") require.NoError(t, err) found, err := models.FindDeployment(context.Background(), testDB, deployment.ID) require.NoError(t, err) assert.Contains(t, found.Logs.String, "Building image...") assert.Contains(t, found.Logs.String, "Image built successfully") } func TestDeploymentMarkFinished(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) deployment := models.NewDeployment(testDB) deployment.AppID = app.ID deployment.Status = models.DeploymentStatusBuilding err := deployment.Save(context.Background()) require.NoError(t, err) err = deployment.MarkFinished(context.Background(), models.DeploymentStatusSuccess) require.NoError(t, err) found, err := models.FindDeployment(context.Background(), testDB, deployment.ID) require.NoError(t, err) assert.Equal(t, models.DeploymentStatusSuccess, found.Status) assert.True(t, found.FinishedAt.Valid) } func TestDeploymentFindByAppID(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) for idx := range 5 { deploy := models.NewDeployment(testDB) deploy.AppID = app.ID deploy.Status = models.DeploymentStatusSuccess deploy.CommitSHA = sql.NullString{ String: "commit" + strconv.Itoa(idx), Valid: true, } err := deploy.Save(context.Background()) require.NoError(t, err) } deployments, err := models.FindDeploymentsByAppID(context.Background(), testDB, app.ID, 3) require.NoError(t, err) assert.Len(t, deployments, 3) } func TestDeploymentFindLatest(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) for idx := range 3 { deploy := models.NewDeployment(testDB) deploy.AppID = app.ID deploy.CommitSHA = sql.NullString{ String: "commit" + strconv.Itoa(idx), Valid: true, } deploy.Status = models.DeploymentStatusSuccess err := deploy.Save(context.Background()) require.NoError(t, err) } latest, err := models.LatestDeploymentForApp(context.Background(), testDB, app.ID) require.NoError(t, err) require.NotNil(t, latest) assert.Equal(t, "commit2", latest.CommitSHA.String) } // App Helper Methods Tests. func TestAppGetEnvVars(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) env1 := models.NewEnvVar(testDB) env1.AppID = app.ID env1.Key = "KEY1" env1.Value = "value1" _ = env1.Save(context.Background()) env2 := models.NewEnvVar(testDB) env2.AppID = app.ID env2.Key = "KEY2" env2.Value = "value2" _ = env2.Save(context.Background()) envVars, err := app.GetEnvVars(context.Background()) require.NoError(t, err) assert.Len(t, envVars, 2) } func TestAppGetLabels(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) label := models.NewLabel(testDB) label.AppID = app.ID label.Key = "label.key" label.Value = "label.value" _ = label.Save(context.Background()) labels, err := app.GetLabels(context.Background()) require.NoError(t, err) assert.Len(t, labels, 1) } func TestAppGetVolumes(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) vol := models.NewVolume(testDB) vol.AppID = app.ID vol.HostPath = "/host" vol.ContainerPath = "/container" _ = vol.Save(context.Background()) volumes, err := app.GetVolumes(context.Background()) require.NoError(t, err) assert.Len(t, volumes, 1) } func TestAppGetDeployments(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) deploy := models.NewDeployment(testDB) deploy.AppID = app.ID deploy.Status = models.DeploymentStatusSuccess _ = deploy.Save(context.Background()) deployments, err := app.GetDeployments(context.Background(), 10) require.NoError(t, err) assert.Len(t, deployments, 1) } func TestAppGetWebhookEvents(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) event := models.NewWebhookEvent(testDB) event.AppID = app.ID event.EventType = testEventType event.Branch = testBranch event.Matched = true _ = event.Save(context.Background()) events, err := app.GetWebhookEvents(context.Background(), 10) require.NoError(t, err) assert.Len(t, events, 1) } // Cascade Delete Tests. //nolint:funlen // Test function with many assertions - acceptable for integration tests func TestCascadeDelete(t *testing.T) { t.Parallel() t.Run("deleting app cascades to related records", func(t *testing.T) { t.Parallel() testDB, cleanup := setupTestDB(t) defer cleanup() app := createTestApp(t, testDB) // Create related records. env := models.NewEnvVar(testDB) env.AppID = app.ID env.Key = "KEY" env.Value = "value" _ = env.Save(context.Background()) label := models.NewLabel(testDB) label.AppID = app.ID label.Key = "key" label.Value = "value" _ = label.Save(context.Background()) vol := models.NewVolume(testDB) vol.AppID = app.ID vol.HostPath = "/host" vol.ContainerPath = "/container" _ = vol.Save(context.Background()) event := models.NewWebhookEvent(testDB) event.AppID = app.ID event.EventType = testEventType event.Branch = testBranch event.Matched = true _ = event.Save(context.Background()) deploy := models.NewDeployment(testDB) deploy.AppID = app.ID deploy.Status = models.DeploymentStatusSuccess _ = deploy.Save(context.Background()) // Delete app. err := app.Delete(context.Background()) require.NoError(t, err) // Verify cascades. envVars, _ := models.FindEnvVarsByAppID( context.Background(), testDB, app.ID, ) assert.Empty(t, envVars) labels, _ := models.FindLabelsByAppID( context.Background(), testDB, app.ID, ) assert.Empty(t, labels) volumes, _ := models.FindVolumesByAppID( context.Background(), testDB, app.ID, ) assert.Empty(t, volumes) events, _ := models.FindWebhookEventsByAppID( context.Background(), testDB, app.ID, 10, ) assert.Empty(t, events) deployments, _ := models.FindDeploymentsByAppID( context.Background(), testDB, app.ID, 10, ) assert.Empty(t, deployments) }) } // Helper function to create a test app. func createTestApp(t *testing.T, testDB *database.Database) *models.App { t.Helper() app := models.NewApp(testDB) app.ID = "test-app-" + t.Name() app.Name = "test-app" app.RepoURL = "git@example.com:user/repo.git" app.Branch = testBranch app.DockerfilePath = "Dockerfile" app.WebhookSecret = "secret-" + t.Name() app.SSHPrivateKey = "private" app.SSHPublicKey = "public" err := app.Save(context.Background()) require.NoError(t, err) return app }