From 4f25498e71d7779be53b7eefc39ceaddfde9505d Mon Sep 17 00:00:00 2001 From: 07calc Date: Sun, 18 Jan 2026 21:23:24 +0530 Subject: [PATCH] test: db tests --- server/db/main.go | 2 +- server/db/migrations.go | 6 +- test/database/database_test.go | 2305 ++++++++++++++++++++++++++++++++ test/go.mod | 53 +- test/go.sum | 113 +- test/main.go | 1 - 6 files changed, 2348 insertions(+), 132 deletions(-) create mode 100644 test/database/database_test.go delete mode 100644 test/main.go diff --git a/server/db/main.go b/server/db/main.go index a7fb9f7..8ff8189 100644 --- a/server/db/main.go +++ b/server/db/main.go @@ -30,7 +30,7 @@ func InitDB() (*gorm.DB, error) { if err != nil { return nil, fmt.Errorf("failed to open database: %v", err) } - err = MigrateDb(db) + err = MigrateDB(db) if err != nil { return nil, err } diff --git a/server/db/migrations.go b/server/db/migrations.go index e5efc68..4201ff1 100644 --- a/server/db/migrations.go +++ b/server/db/migrations.go @@ -88,7 +88,11 @@ func getSQLiteColumnType(field *schema.Field) string { } } -func MigrateDb(dbInstance *gorm.DB) error { +func MigrateDB(dbInstance *gorm.DB) error { + return migrateDbInternal(dbInstance) +} + +func migrateDbInternal(dbInstance *gorm.DB) error { migrator := dbInstance.Migrator() allModels := []interface{}{ diff --git a/test/database/database_test.go b/test/database/database_test.go new file mode 100644 index 0000000..0573daf --- /dev/null +++ b/test/database/database_test.go @@ -0,0 +1,2305 @@ +package db + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + mistdb "github.com/corecollectives/mist/db" + "github.com/corecollectives/mist/models" + "github.com/corecollectives/mist/utils" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func setupTestDB(t *testing.T) *gorm.DB { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + + db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + t.Fatalf("failed to open test database: %v", err) + } + + err = mistdb.MigrateDB(db) + if err != nil { + t.Fatalf("failed to run migrations: %v", err) + } + + models.SetDB(db) + return db +} + +func cleanupDB(db *gorm.DB) { + sqlDB, _ := db.DB() + sqlDB.Close() +} + +func TestInitDB_Success(t *testing.T) { + os.Setenv("ENV", "dev") + defer os.Unsetenv("ENV") + + db, err := mistdb.InitDB() + if err != nil { + t.Fatalf("InitDB failed: %v", err) + } + defer cleanupDB(db) + + if db == nil { + t.Fatal("db should not be nil") + } +} + +func TestInitDB_FailsOnInvalidPath(t *testing.T) { + origEnv := os.Getenv("ENV") + os.Setenv("ENV", "dev") + defer os.Setenv("ENV", origEnv) + + t.Skip("Skipping test that depends on filesystem permissions") +} + +func TestMigrateDb_CreatesAllTables(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + expectedTables := []string{ + "users", "api_tokens", "apps", "audit_logs", "backups", + "deployments", "envs", "projects", "project_members", + "git_providers", "github_installations", "app_repositories", "domains", + "volumes", "crons", "registries", "sessions", "notifications", + } + + for _, table := range expectedTables { + if !db.Migrator().HasTable(table) { + t.Errorf("table %s should exist", table) + } + } +} + +func TestMigrateDb_SkipsExistingTables(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + err := mistdb.MigrateDB(db) + if err != nil { + t.Errorf("re-running migrations should not fail: %v", err) + } +} + +func TestUser_Create(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "testuser", + Email: "test@example.com", + PasswordHash: "hash", + } + + err := user.Create() + if err != nil { + t.Fatalf("CreateUser failed: %v", err) + } + + if user.ID == 0 { + t.Error("user ID should be set after creation") + } + + if user.Role != "user" { + t.Error("default role should be user") + } +} + +func TestUser_Create_DuplicateUsername(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user1 := &models.User{ + ID: utils.GenerateRandomId(), + Username: "uniqueuser", + Email: "user1@example.com", + PasswordHash: "hash", + } + user2 := &models.User{ + ID: utils.GenerateRandomId(), + Username: "uniqueuser", + Email: "user2@example.com", + PasswordHash: "hash", + } + + if err := user1.Create(); err != nil { + t.Fatalf("first user creation failed: %v", err) + } + + err := user2.Create() + if err == nil { + t.Error("should fail with duplicate username") + } +} + +func TestUser_Create_DuplicateEmail(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user1 := &models.User{ + ID: utils.GenerateRandomId(), + Username: "user1", + Email: "same@example.com", + PasswordHash: "hash", + } + user2 := &models.User{ + ID: utils.GenerateRandomId(), + Username: "user2", + Email: "same@example.com", + PasswordHash: "hash", + } + + if err := user1.Create(); err != nil { + t.Fatalf("first user creation failed: %v", err) + } + + err := user2.Create() + if err == nil { + t.Error("should fail with duplicate email") + } +} + +func TestUser_SetPassword(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "passuser", + Email: "pass@example.com", + } + + err := user.SetPassword("testpassword123") + if err != nil { + t.Fatalf("SetPassword failed: %v", err) + } + + if user.PasswordHash == "testpassword123" { + t.Error("password should not be stored in plaintext") + } + + if len(user.PasswordHash) < 50 { + t.Error("password hash should be properly hashed") + } +} + +func TestUser_MatchPassword_Success(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "matchuser", + Email: "match@example.com", + } + user.SetPassword("correctpassword") + user.Create() + + if !user.MatchPassword("correctpassword") { + t.Error("MatchPassword should return true for correct password") + } +} + +func TestUser_MatchPassword_WrongPassword(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "wrongpass", + Email: "wrong@example.com", + } + user.SetPassword("correctpassword") + user.Create() + + if user.MatchPassword("wrongpassword") { + t.Error("MatchPassword should return false for wrong password") + } +} + +func TestGetUserByID_Exists(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + origUser := &models.User{ + ID: utils.GenerateRandomId(), + Username: "getbyid", + Email: "getbyid@example.com", + PasswordHash: "hash", + Role: "admin", + } + origUser.Create() + + user, err := models.GetUserByID(origUser.ID) + if err != nil { + t.Fatalf("GetUserByID failed: %v", err) + } + + if user.Username != origUser.Username { + t.Error("retrieved user should match original") + } +} + +func TestGetUserByID_NotExists(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + _, err := models.GetUserByID(999999) + if err == nil { + t.Error("should return error for non-existent user") + } +} + +func TestGetUserByEmail_Exists(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + origUser := &models.User{ + ID: utils.GenerateRandomId(), + Username: "emailtest", + Email: "findme@example.com", + PasswordHash: "hash", + } + origUser.Create() + + user, err := models.GetUserByEmail("findme@example.com") + if err != nil { + t.Fatalf("GetUserByEmail failed: %v", err) + } + + if user.ID != origUser.ID { + t.Error("retrieved user should match by email") + } +} + +func TestGetUserByUsername_Exists(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + origUser := &models.User{ + ID: utils.GenerateRandomId(), + Username: "uniquetestuser", + Email: "unique@example.com", + PasswordHash: "hash", + } + origUser.Create() + + user, err := models.GetUserByUsername("uniquetestuser") + if err != nil { + t.Fatalf("GetUserByUsername failed: %v", err) + } + + if user.ID != origUser.ID { + t.Error("retrieved user should match by username") + } +} + +func TestUpdateUser(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "updateme", + Email: "update@example.com", + PasswordHash: "hash", + } + user.Create() + + newUsername := "updatedname" + newEmail := "newemail@example.com" + user.Username = newUsername + user.Email = newEmail + + err := models.UpdateUser(user) + if err != nil { + t.Fatalf("UpdateUser failed: %v", err) + } + + updated, _ := models.GetUserByID(user.ID) + if updated.Username != newUsername { + t.Error("username should be updated") + } + if updated.Email != newEmail { + t.Error("email should be updated") + } +} + +func TestUpdateUserPassword(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "passupdate", + Email: "passupdate@example.com", + } + user.SetPassword("oldpassword") + user.Create() + + user.SetPassword("newpassword") + err := user.UpdatePassword() + if err != nil { + t.Fatalf("UpdatePassword failed: %v", err) + } + + if user.MatchPassword("oldpassword") { + t.Error("old password should not match") + } + if !user.MatchPassword("newpassword") { + t.Error("new password should match") + } +} + +func TestDeleteUser(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deleteme", + Email: "delete@example.com", + PasswordHash: "hash", + } + user.Create() + + err := models.DeleteUserByID(user.ID) + if err != nil { + t.Fatalf("DeleteUser failed: %v", err) + } + + _, err = models.GetUserByID(user.ID) + if err == nil { + t.Error("user should be deleted") + } +} + +func TestGetAllUsers(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + for i := 0; i < 5; i++ { + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "listuser" + string(rune('a'+i)), + Email: "listuser" + string(rune('a'+i)) + "@example.com", + PasswordHash: "hash", + } + user.Create() + } + + users, err := models.GetAllUsers() + if err != nil { + t.Fatalf("GetAllUsers failed: %v", err) + } + + if len(users) < 5 { + t.Error("should return all users") + } +} + +func TestGetUserCount(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + initial, _ := models.GetUserCount() + + for i := 0; i < 3; i++ { + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "countuser" + string(rune('a'+i)), + Email: "countuser" + string(rune('a'+i)) + "@example.com", + PasswordHash: "hash", + } + user.Create() + } + + count, err := models.GetUserCount() + if err != nil { + t.Fatalf("GetUserCount failed: %v", err) + } + + if count != initial+3 { + t.Error("count should include all created users") + } +} + +func TestUserRoleRetrieval(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + adminUser := &models.User{ + ID: utils.GenerateRandomId(), + Username: "adminuser", + Email: "admin@example.com", + PasswordHash: "hash", + Role: "admin", + } + adminUser.Create() + + role, err := models.GetUserRole(adminUser.ID) + if err != nil { + t.Fatalf("GetUserRole failed: %v", err) + } + + if role != "admin" { + t.Error("should return correct role") + } +} + +func TestUser_OptionalFields(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + fullName := "Test Full Name" + avatarURL := "https://example.com/avatar.png" + bio := "Test bio" + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "optionalfields", + Email: "optional@example.com", + PasswordHash: "hash", + FullName: &fullName, + AvatarURL: &avatarURL, + Bio: &bio, + } + user.Create() + + retrieved, _ := models.GetUserByID(user.ID) + if retrieved.FullName == nil || *retrieved.FullName != fullName { + t.Error("FullName should be stored correctly") + } + if retrieved.AvatarURL == nil || *retrieved.AvatarURL != avatarURL { + t.Error("AvatarURL should be stored correctly") + } + if retrieved.Bio == nil || *retrieved.Bio != bio { + t.Error("Bio should be stored correctly") + } +} + +func TestProject_Insert(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "projectowner", + Email: "projectowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + desc := "Test project description" + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Test Project", + Description: &desc, + OwnerID: owner.ID, + Tags: []string{"tag1", "tag2"}, + } + + err := project.InsertInDB() + if err != nil { + t.Fatalf("InsertInDB failed: %v", err) + } + + if project.Owner == nil { + t.Error("owner should be populated after insert") + } +} + +func TestProject_GetByID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "getprojectowner", + Email: "getprojectowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Get Project Test", + OwnerID: owner.ID, + Tags: []string{"test"}, + } + project.InsertInDB() + + retrieved, err := models.GetProjectByID(project.ID) + if err != nil { + t.Fatalf("GetProjectByID failed: %v", err) + } + + if retrieved.Name != project.Name { + t.Error("project name should match") + } +} + +func TestProject_Update(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "updateprojectowner", + Email: "updateprojectowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Original Name", + OwnerID: owner.ID, + } + project.InsertInDB() + + newName := "Updated Name" + newTags := []string{"newtag"} + project.Name = newName + project.Tags = newTags + + err := models.UpdateProject(project) + if err != nil { + t.Fatalf("UpdateProject failed: %v", err) + } + + updated, _ := models.GetProjectByID(project.ID) + if updated.Name != newName { + t.Error("name should be updated") + } +} + +func TestProject_Delete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deleteprojectowner", + Email: "deleteprojectowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Delete Project Test", + OwnerID: owner.ID, + } + project.InsertInDB() + + err := models.DeleteProjectByID(project.ID) + if err != nil { + t.Fatalf("DeleteProject failed: %v", err) + } + + _, err = models.GetProjectByID(project.ID) + if err == nil { + t.Error("project should be deleted") + } +} + +func TestProject_HasUserAccess(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "accessuser", + Email: "accessuser@example.com", + PasswordHash: "hash", + } + user.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Access Project", + OwnerID: user.ID, + } + project.InsertInDB() + + hasAccess, err := models.HasUserAccessToProject(user.ID, project.ID) + if err != nil { + t.Fatalf("HasUserAccessToProject failed: %v", err) + } + + if !hasAccess { + t.Error("owner should have access to project") + } +} + +func TestProject_IsUserOwner(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "owneruser", + Email: "owneruser@example.com", + PasswordHash: "hash", + } + user.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Owner Project", + OwnerID: user.ID, + } + project.InsertInDB() + + isOwner, err := models.IsUserProjectOwner(user.ID, project.ID) + if err != nil { + t.Fatalf("IsUserProjectOwner failed: %v", err) + } + + if !isOwner { + t.Error("user should be project owner") + } +} + +func TestProject_UpdateMembers(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "memberowner", + Email: "memberowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + member1 := &models.User{ + ID: utils.GenerateRandomId(), + Username: "member1", + Email: "member1@example.com", + PasswordHash: "hash", + } + member1.Create() + + member2 := &models.User{ + ID: utils.GenerateRandomId(), + Username: "member2", + Email: "member2@example.com", + PasswordHash: "hash", + } + member2.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Members Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + err := models.UpdateProjectMembers(project.ID, []int64{member1.ID, member2.ID}) + if err != nil { + t.Fatalf("UpdateProjectMembers failed: %v", err) + } + + retrieved, _ := models.GetProjectByID(project.ID) + if len(retrieved.ProjectMembers) < 2 { + t.Error("project should have members") + } +} + +func TestProject_OwnerAlwaysIncludedInMembers(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "forcedowner", + Email: "forcedowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Forced Owner Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + err := models.UpdateProjectMembers(project.ID, []int64{}) + if err != nil { + t.Fatalf("UpdateProjectMembers failed: %v", err) + } + + retrieved, _ := models.GetProjectByID(project.ID) + found := false + for _, member := range retrieved.ProjectMembers { + if member.ID == owner.ID { + found = true + break + } + } + + if !found { + t.Error("owner should always be included in members") + } +} + +func TestProject_GetProjectsUserIsPartOf(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "partofuser", + Email: "partofuser@example.com", + PasswordHash: "hash", + } + user.Create() + + project1 := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Project 1", + OwnerID: user.ID, + } + project1.InsertInDB() + + projects, err := models.GetProjectsUserIsPartOf(user.ID) + if err != nil { + t.Fatalf("GetProjectsUserIsPartOf failed: %v", err) + } + + if len(projects) < 1 { + t.Error("should return projects user is part of") + } +} + +func TestApp_Insert(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "appowner", + Email: "appowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "App Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Test App", + CreatedBy: owner.ID, + AppType: models.AppTypeWeb, + GitBranch: "main", + } + + err := app.InsertInDB() + if err != nil { + t.Fatalf("InsertInDB failed: %v", err) + } + + if app.ID == 0 { + t.Error("app ID should be set") + } + if app.AppType != models.AppTypeWeb { + t.Error("default app type should be web") + } + if app.Status != models.StatusStopped { + t.Error("default status should be stopped") + } +} + +func TestApp_GetByID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "getappowner", + Email: "getappowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Get App Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Get App Test", + CreatedBy: owner.ID, + } + app.InsertInDB() + + retrieved, err := models.GetApplicationByID(app.ID) + if err != nil { + t.Fatalf("GetApplicationByID failed: %v", err) + } + + if retrieved.Name != app.Name { + t.Error("retrieved app should match") + } +} + +func TestApp_GetByProjectID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "listappowner", + Email: "listappowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "List App Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + for i := 0; i < 3; i++ { + app := &models.App{ + ProjectID: project.ID, + Name: "App " + string(rune('a'+i)), + CreatedBy: owner.ID, + } + app.InsertInDB() + } + + apps, err := models.GetApplicationByProjectID(project.ID) + if err != nil { + t.Fatalf("GetApplicationByProjectID failed: %v", err) + } + + if len(apps) < 3 { + t.Error("should return all apps for project") + } +} + +func TestApp_Update(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "updateappowner", + Email: "updateappowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Update App Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Original App Name", + CreatedBy: owner.ID, + } + app.InsertInDB() + + newName := "Updated App Name" + port := int64(8080) + app.Name = newName + app.Port = &port + + err := app.UpdateApplication() + if err != nil { + t.Fatalf("UpdateApplication failed: %v", err) + } + + updated, _ := models.GetApplicationByID(app.ID) + if updated.Name != newName { + t.Error("name should be updated") + } + if updated.Port == nil || *updated.Port != 8080 { + t.Error("port should be updated") + } +} + +func TestApp_Delete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deleteappowner", + Email: "deleteappowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Delete App Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Delete App Test", + CreatedBy: owner.ID, + } + app.InsertInDB() + + err := models.DeleteApplication(app.ID) + if err != nil { + t.Fatalf("DeleteApplication failed: %v", err) + } + + _, err = models.GetApplicationByID(app.ID) + if err == nil { + t.Error("app should be deleted") + } +} + +func TestApp_IsUserOwner(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "appownerverify", + Email: "appownerverify@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "App Owner Verify Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "App Owner Verify Test", + CreatedBy: owner.ID, + } + app.InsertInDB() + + isOwner, err := models.IsUserApplicationOwner(owner.ID, app.ID) + if err != nil { + t.Fatalf("IsUserApplicationOwner failed: %v", err) + } + + if !isOwner { + t.Error("user should be app owner") + } +} + +func TestApp_CascadingDelete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "cascadeappowner", + Email: "cascadeappowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Cascade App Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Cascade App Test", + CreatedBy: owner.ID, + } + app.InsertInDB() + + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "abc123", + } + deployment.CreateDeployment() + + models.DeleteApplication(app.ID) + + var appCount int64 + db.Model(&models.App{}).Where("id = ?", app.ID).Count(&appCount) + if appCount != 0 { + t.Error("app should be deleted") + } +} + +func TestDeployment_Create(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deployowner", + Email: "deployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "abc123def456", + TriggeredBy: &owner.ID, + } + + err := deployment.CreateDeployment() + if err != nil { + t.Fatalf("CreateDeployment failed: %v", err) + } + + if deployment.DeploymentNumber == nil { + t.Error("deployment number should be set") + } + if *deployment.DeploymentNumber != 1 { + t.Error("first deployment should have number 1") + } + if deployment.Status != models.DeploymentStatusPending { + t.Error("default status should be pending") + } +} + +func TestDeployment_AutoIncrementNumber(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "multideployowner", + Email: "multideployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Multi Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Multi Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + for i := 0; i < 3; i++ { + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: fmt.Sprintf("commit_%d_%d", time.Now().UnixNano(), i), + } + err := deployment.CreateDeployment() + if err != nil { + t.Fatalf("deployment creation failed: %v", err) + } + + if deployment.DeploymentNumber == nil { + t.Errorf("deployment %d: deployment number is nil", i) + } else { + t.Logf("deployment %d: got number %d", i, *deployment.DeploymentNumber) + } + } + + deployments, _ := models.GetDeploymentsByAppID(app.ID) + if len(deployments) != 3 { + t.Errorf("expected 3 deployments, got %d", len(deployments)) + } +} + +func TestDeployment_GetByID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "getdeployowner", + Email: "getdeployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Get Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Get Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "getdeploy123", + } + deployment.CreateDeployment() + + retrieved, err := models.GetDeploymentByID(deployment.ID) + if err != nil { + t.Fatalf("GetDeploymentByID failed: %v", err) + } + + if retrieved.CommitHash != deployment.CommitHash { + t.Error("retrieved deployment should match") + } +} + +func TestDeployment_GetByAppID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "listdeployowner", + Email: "listdeployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "List Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "List Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + for i := 0; i < 3; i++ { + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "listdeploy" + string(rune('a'+i)), + } + deployment.CreateDeployment() + } + + deployments, err := models.GetDeploymentsByAppID(app.ID) + if err != nil { + t.Fatalf("GetDeploymentsByAppID failed: %v", err) + } + + if len(deployments) < 3 { + t.Error("should return all deployments for app") + } +} + +func TestDeployment_UpdateStatus(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "statusdeployowner", + Email: "statusdeployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Status Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Status Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "statusdeploy123", + } + deployment.CreateDeployment() + + err := models.UpdateDeploymentStatus(deployment.ID, string(models.DeploymentStatusSuccess), "completed", 100, nil) + if err != nil { + t.Fatalf("UpdateDeploymentStatus failed: %v", err) + } + + updated, _ := models.GetDeploymentByID(deployment.ID) + if updated.Status != models.DeploymentStatusSuccess { + t.Error("status should be updated") + } + if updated.FinishedAt == nil { + t.Error("finished_at should be set on completion") + } +} + +func TestDeployment_MarkActive(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "activedeployowner", + Email: "activedeployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Active Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Active Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + deployment1 := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "active1", + } + deployment1.CreateDeployment() + + deployment2 := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "active2", + } + deployment2.CreateDeployment() + + err := models.MarkDeploymentActive(deployment1.ID, app.ID) + if err != nil { + t.Fatalf("MarkDeploymentActive failed: %v", err) + } + + d1, _ := models.GetDeploymentByID(deployment1.ID) + d2, _ := models.GetDeploymentByID(deployment2.ID) + + if !d1.IsActive { + t.Error("first deployment should be active") + } + if d2.IsActive { + t.Error("second deployment should not be active") + } +} + +func TestDeployment_GetIncomplete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "incompletedeployowner", + Email: "incompletedeployowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Incomplete Deploy Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Incomplete Deploy App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + deployment1 := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "incomplete1", + } + deployment1.CreateDeployment() + + deployment2 := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: "incomplete2", + } + deployment2.CreateDeployment() + + models.UpdateDeploymentStatus(deployment2.ID, string(models.DeploymentStatusBuilding), "building", 50, nil) + + incomplete, err := models.GetIncompleteDeployments() + if err != nil { + t.Fatalf("GetIncompleteDeployments failed: %v", err) + } + + found := false + for _, d := range incomplete { + if d.ID == deployment2.ID && d.AppID == app.ID { + found = true + break + } + } + + if !found { + t.Logf("Incomplete deployments found: %d", len(incomplete)) + for _, d := range incomplete { + t.Logf(" - ID: %d, AppID: %d, Status: %s", d.ID, d.AppID, d.Status) + } + t.Error("should return incomplete deployments") + } +} + +func TestDomain_Create(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "domainowner", + Email: "domainowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Domain Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Domain App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + domain, err := models.CreateDomain(app.ID, "example.com") + if err != nil { + t.Fatalf("CreateDomain failed: %v", err) + } + + if domain.Domain != "example.com" { + t.Error("domain name should match") + } + if domain.AppID != app.ID { + t.Error("app id should match") + } + if domain.SslStatus != models.SSLStatusPending { + t.Error("default ssl status should be pending") + } +} + +func TestDomain_GetByAppID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "listdomainowner", + Email: "listdomainowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "List Domain Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "List Domain App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + models.CreateDomain(app.ID, "domain1.example.com") + models.CreateDomain(app.ID, "domain2.example.com") + + domains, err := models.GetDomainsByAppID(app.ID) + if err != nil { + t.Fatalf("GetDomainsByAppID failed: %v", err) + } + + if len(domains) < 2 { + t.Error("should return all domains for app") + } +} + +func TestDomain_GetPrimary(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "primarydomainowner", + Email: "primarydomainowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Primary Domain Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Primary Domain App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + models.CreateDomain(app.ID, "primary.example.com") + models.CreateDomain(app.ID, "secondary.example.com") + + primary, err := models.GetPrimaryDomainByAppID(app.ID) + if err != nil { + t.Fatalf("GetPrimaryDomainByAppID failed: %v", err) + } + + if primary.Domain != "primary.example.com" { + t.Error("first created domain should be primary") + } +} + +func TestDomain_Update(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "updatedomainowner", + Email: "updatedomainowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Update Domain Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Update Domain App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + domain, _ := models.CreateDomain(app.ID, "olddomain.com") + + err := models.UpdateDomain(domain.ID, "newdomain.com") + if err != nil { + t.Fatalf("UpdateDomain failed: %v", err) + } + + updated, _ := models.GetDomainByID(domain.ID) + if updated.Domain != "newdomain.com" { + t.Error("domain should be updated") + } +} + +func TestDomain_Delete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deletedomainowner", + Email: "deletedomainowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Delete Domain Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Delete Domain App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + domain, _ := models.CreateDomain(app.ID, "deleteme.com") + + err := models.DeleteDomain(domain.ID) + if err != nil { + t.Fatalf("DeleteDomain failed: %v", err) + } + + _, err = models.GetDomainByID(domain.ID) + if err == nil { + t.Error("domain should be deleted") + } +} + +func TestDomain_UniqueConstraint(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "uniquedomainowner", + Email: "uniquedomainowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Unique Domain Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app1 := &models.App{ + ProjectID: project.ID, + Name: "Unique Domain App 1", + CreatedBy: owner.ID, + } + app1.InsertInDB() + + app2 := &models.App{ + ProjectID: project.ID, + Name: "Unique Domain App 2", + CreatedBy: owner.ID, + } + app2.InsertInDB() + + _, err := models.CreateDomain(app1.ID, "same-domain.com") + if err != nil { + t.Fatalf("first domain creation failed: %v", err) + } + + _, err = models.CreateDomain(app2.ID, "same-domain.com") + if err == nil { + t.Error("should fail with duplicate domain") + } +} + +func TestEnvVariable_Create(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "envarowner", + Email: "envarowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "EnvVar Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "EnvVar App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + env, err := models.CreateEnvVariable(app.ID, "TEST_VAR", "test_value") + if err != nil { + t.Fatalf("CreateEnvVariable failed: %v", err) + } + + if env.Key != "TEST_VAR" { + t.Error("key should match") + } + if env.Value != "test_value" { + t.Error("value should match") + } +} + +func TestEnvVariable_GetByAppID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "listenvarowner", + Email: "listenvarowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "List EnvVar Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "List EnvVar App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + models.CreateEnvVariable(app.ID, "VAR1", "val1") + models.CreateEnvVariable(app.ID, "VAR2", "val2") + models.CreateEnvVariable(app.ID, "VAR3", "val3") + + envs, err := models.GetEnvVariablesByAppID(app.ID) + if err != nil { + t.Fatalf("GetEnvVariablesByAppID failed: %v", err) + } + + if len(envs) < 3 { + t.Error("should return all env variables") + } +} + +func TestEnvVariable_Update(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "updateenvarowner", + Email: "updateenvarowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Update EnvVar Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Update EnvVar App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + env, _ := models.CreateEnvVariable(app.ID, "UPDATEME", "oldvalue") + + err := models.UpdateEnvVariable(env.ID, "UPDATEME", "newvalue") + if err != nil { + t.Fatalf("UpdateEnvVariable failed: %v", err) + } + + updated, _ := models.GetEnvVariableByID(env.ID) + if updated.Value != "newvalue" { + t.Error("value should be updated") + } +} + +func TestEnvVariable_Delete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deleteenvarowner", + Email: "deleteenvarowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Delete EnvVar Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Delete EnvVar App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + env, _ := models.CreateEnvVariable(app.ID, "DELETEME", "value") + + err := models.DeleteEnvVariable(env.ID) + if err != nil { + t.Fatalf("DeleteEnvVariable failed: %v", err) + } + + _, err = models.GetEnvVariableByID(env.ID) + if err == nil { + t.Error("env variable should be deleted") + } +} + +func TestEnvVariable_UniqueConstraint(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "uniqueenvarowner", + Email: "uniqueenvarowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Unique EnvVar Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Unique EnvVar App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + _, err := models.CreateEnvVariable(app.ID, "UNIQUE_VAR", "value1") + if err != nil { + t.Fatalf("first env var creation failed: %v", err) + } + + _, err = models.CreateEnvVariable(app.ID, "UNIQUE_VAR", "value2") + if err == nil { + t.Error("should fail with duplicate key for same app") + } +} + +func TestVolume_Create(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "volumeowner", + Email: "volumeowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Volume Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Volume App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + vol, err := models.CreateVolume(app.ID, "data", "/host/data", "/container/data", false) + if err != nil { + t.Fatalf("CreateVolume failed: %v", err) + } + + if vol.Name != "data" { + t.Error("name should match") + } + if vol.HostPath != "/host/data" { + t.Error("host path should match") + } + if vol.ContainerPath != "/container/data" { + t.Error("container path should match") + } +} + +func TestVolume_GetByAppID(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "listvolumeowner", + Email: "listvolumeowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "List Volume Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "List Volume App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + models.CreateVolume(app.ID, "vol1", "/h1", "/c1", false) + models.CreateVolume(app.ID, "vol2", "/h2", "/c2", true) + + vols, err := models.GetVolumesByAppID(app.ID) + if err != nil { + t.Fatalf("GetVolumesByAppID failed: %v", err) + } + + if len(vols) < 2 { + t.Error("should return all volumes") + } +} + +func TestVolume_Update(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "updatevolumeowner", + Email: "updatevolumeowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Update Volume Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Update Volume App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + vol, _ := models.CreateVolume(app.ID, "oldname", "/old/host", "/old/container", false) + + err := models.UpdateVolume(vol.ID, "newname", "/new/host", "/new/container", true) + if err != nil { + t.Fatalf("UpdateVolume failed: %v", err) + } + + updated, _ := models.GetVolumeByID(vol.ID) + if updated.Name != "newname" { + t.Error("name should be updated") + } + if updated.ReadOnly != true { + t.Error("readOnly should be updated") + } +} + +func TestVolume_Delete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + owner := &models.User{ + ID: utils.GenerateRandomId(), + Username: "deletevolumeowner", + Email: "deletevolumeowner@example.com", + PasswordHash: "hash", + } + owner.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Delete Volume Project", + OwnerID: owner.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Delete Volume App", + CreatedBy: owner.ID, + } + app.InsertInDB() + + vol, _ := models.CreateVolume(app.ID, "deleteme", "/host", "/container", false) + + err := models.DeleteVolume(vol.ID) + if err != nil { + t.Fatalf("DeleteVolume failed: %v", err) + } + + result, err := models.GetVolumeByID(vol.ID) + if err != nil { + t.Fatalf("GetVolumeByID failed: %v", err) + } + if result != nil { + t.Error("volume should be deleted") + } +} + +func TestTransaction_Success(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "transuser", + Email: "trans@example.com", + PasswordHash: "hash", + } + user.Create() + + err := db.Transaction(func(tx *gorm.DB) error { + user.Role = "admin" + return tx.Save(user).Error + }) + + if err != nil { + t.Fatalf("Transaction failed: %v", err) + } + + updated, _ := models.GetUserByID(user.ID) + if updated.Role != "admin" { + t.Error("role should be updated within transaction") + } +} + +func TestTransaction_Rollback(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "rollbackuser", + Email: "rollback@example.com", + PasswordHash: "hash", + } + user.Create() + + originalRole := user.Role + + err := db.Transaction(func(tx *gorm.DB) error { + user.Role = "admin" + if err := tx.Save(user).Error; err != nil { + return err + } + return fmt.Errorf("force rollback") + }) + + if err == nil { + t.Error("transaction should fail") + } + + updated, _ := models.GetUserByID(user.ID) + if updated.Role != originalRole { + t.Error("changes should be rolled back") + } +} + +func TestSoftDelete(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "softdeleteuser", + Email: "softdelete@example.com", + PasswordHash: "hash", + } + user.Create() + + db.Delete(user) + + var count int64 + db.Model(&models.User{}).Where("id = ?", user.ID).Count(&count) + if count != 0 { + t.Error("soft deleted user should not be found with regular query") + } + + var deleted models.User + db.Unscoped().Where("id = ?", user.ID).First(&deleted) + if deleted.ID != user.ID { + t.Error("soft deleted user should be retrievable with Unscoped") + } +} + +func TestConcurrentWrites(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "concurrentuser", + Email: "concurrent@example.com", + PasswordHash: "hash", + } + user.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Concurrent Project", + OwnerID: user.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Concurrent App", + CreatedBy: user.ID, + } + app.InsertInDB() + + results := make(chan error, 10) + + for i := 0; i < 10; i++ { + go func(idx int) { + deployment := &models.Deployment{ + ID: utils.GenerateRandomId(), + AppID: app.ID, + CommitHash: fmt.Sprintf("commit%d", idx), + } + results <- deployment.CreateDeployment() + }(i) + } + + for i := 0; i < 10; i++ { + if err := <-results; err != nil { + t.Errorf("concurrent write failed: %v", err) + } + } + + deployments, _ := models.GetDeploymentsByAppID(app.ID) + if len(deployments) != 10 { + t.Error("all concurrent deployments should be created") + } +} + +func TestBoundaryValues(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "boundaryuser", + Email: "boundary@example.com", + PasswordHash: "hash", + } + user.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Boundary Project", + OwnerID: user.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Boundary App", + CreatedBy: user.ID, + CPULimit: float64Ptr(0.0), + MemoryLimit: intPtr(0), + } + app.InsertInDB() + + retrieved, _ := models.GetApplicationByID(app.ID) + if retrieved.CPULimit != nil && *retrieved.CPULimit != 0.0 { + t.Error("zero CPU limit should be preserved") + } + if retrieved.MemoryLimit != nil && *retrieved.MemoryLimit != 0 { + t.Error("zero memory limit should be preserved") + } +} + +func float64Ptr(f float64) *float64 { + return &f +} + +func intPtr(i int) *int { + return &i +} + +func TestLargePayload(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "largeuser", + Email: "large@example.com", + PasswordHash: "hash", + } + user.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Large Project", + OwnerID: user.ID, + Tags: make([]string, 100), + } + for i := range project.Tags { + project.Tags[i] = fmt.Sprintf("tag%d", i) + } + project.InsertInDB() + + retrieved, _ := models.GetProjectByID(project.ID) + if len(retrieved.Tags) != 100 { + t.Error("all tags should be stored and retrieved") + } +} + +func TestNullFields(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "nulluser", + Email: "null@example.com", + PasswordHash: "hash", + FullName: nil, + AvatarURL: nil, + Bio: nil, + } + user.Create() + + retrieved, _ := models.GetUserByID(user.ID) + if retrieved.FullName != nil { + t.Error("nil FullName should remain nil") + } + if retrieved.AvatarURL != nil { + t.Error("nil AvatarURL should remain nil") + } + if retrieved.Bio != nil { + t.Error("nil Bio should remain nil") + } +} + +func TestRelatedRecordsCascade(t *testing.T) { + db := setupTestDB(t) + defer cleanupDB(db) + + user := &models.User{ + ID: utils.GenerateRandomId(), + Username: "cascadeuser", + Email: "cascade@example.com", + PasswordHash: "hash", + } + user.Create() + + project := &models.Project{ + ID: utils.GenerateRandomId(), + Name: "Cascade Project", + OwnerID: user.ID, + } + project.InsertInDB() + + app := &models.App{ + ProjectID: project.ID, + Name: "Cascade App", + CreatedBy: user.ID, + } + app.InsertInDB() + + models.CreateDomain(app.ID, "cascade.example.com") + models.CreateVolume(app.ID, "data", "/host", "/container", false) + + var envCount, domainCount, volumeCount int64 + db.Model(&models.EnvVariable{}).Where("app_id = ?", app.ID).Count(&envCount) + db.Model(&models.Domain{}).Where("app_id = ?", app.ID).Count(&domainCount) + db.Model(&models.Volume{}).Where("app_id = ?", app.ID).Count(&volumeCount) + + if envCount != 0 || domainCount != 1 || volumeCount != 1 { + t.Error("related records should be created") + } + + models.DeleteApplication(app.ID) + + var appCount int64 + db.Model(&models.App{}).Where("id = ?", app.ID).Count(&appCount) + if appCount != 0 { + t.Error("app should be deleted") + } + +} diff --git a/test/go.mod b/test/go.mod index 9503784..d8929e6 100644 --- a/test/go.mod +++ b/test/go.mod @@ -5,51 +5,24 @@ go 1.25.1 replace github.com/corecollectives/mist => ../server require ( - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.3.0 // indirect - github.com/cloudflare/circl v1.6.1 // indirect - github.com/containerd/errdefs v1.0.0 // indirect - github.com/containerd/errdefs/pkg v0.3.0 // indirect - github.com/corecollectives/mist v0.0.0 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect - github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.6.0 // indirect - github.com/docker/go-units v0.5.0 // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-git/gcfg/v2 v2.0.2 // indirect - github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd // indirect - github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 // indirect - github.com/go-logr/logr v1.4.2 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/corecollectives/mist v0.0.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/kevinburke/ssh_config v1.4.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-sqlite3 v1.14.32 // indirect - github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/moby/api v1.52.0 // indirect - github.com/moby/moby/client v0.2.1 // indirect - github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/mattn/go-sqlite3 v1.14.33 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/zerolog v1.34.0 // indirect - github.com/sergi/go-diff v1.4.0 // indirect - github.com/shirou/gopsutil v3.21.11+incompatible // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.35.0 // indirect - go.opentelemetry.io/otel/metric v1.35.0 // indirect - go.opentelemetry.io/otel/trace v1.35.0 // indirect golang.org/x/crypto v0.46.0 // indirect - golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/go.sum b/test/go.sum index 7ddb3ac..1b6c656 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,114 +1,49 @@ -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= -github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= -github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= -github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= -github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= -github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= -github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= -github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= -github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= -github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd h1:Gd/f9cGi/3h1JOPaa6er+CkKUGyGX2DBJdFbDKVO+R0= -github.com/go-git/go-billy/v6 v6.0.0-20251217170237-e9738f50a3cd/go.mod h1:d3XQcsHu1idnquxt48kAv+h+1MUiYKLH/e7LAzjP+pI= -github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19 h1:0lz2eJScP8v5YZQsrEw+ggWC5jNySjg4bIZo5BIh6iI= -github.com/go-git/go-git/v6 v6.0.0-20251231065035-29ae690a9f19/go.mod h1:L+Evfcs7EdTqxwv854354cb6+++7TFL3hJn3Wy4g+3w= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ= -github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= -github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg= -github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k= -github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE= -github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= -github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= -github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= -github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= -github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= -go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= -go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= -go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= -go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= -go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= -go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/test/main.go b/test/main.go deleted file mode 100644 index 56e5404..0000000 --- a/test/main.go +++ /dev/null @@ -1 +0,0 @@ -package test