package store import ( "testing" ) func newTestStore(t *testing.T) *Store { t.Helper() s, err := New(":memory:") if err != nil { t.Fatalf("create test store: %v", err) } t.Cleanup(func() { s.Close() }) return s } func TestCreateAndGetProject(t *testing.T) { s := newTestStore(t) p, err := s.CreateProject(Project{ Name: "test-project", Image: "nginx", Port: 80, Env: "{}", Volumes: "{}", }) if err != nil { t.Fatalf("CreateProject: %v", err) } if p.ID == "" { t.Fatal("project ID should be set") } got, err := s.GetProjectByID(p.ID) if err != nil { t.Fatalf("GetProjectByID: %v", err) } if got.Name != "test-project" { t.Fatalf("got name %q, want %q", got.Name, "test-project") } } func TestGetAllProjects(t *testing.T) { s := newTestStore(t) s.CreateProject(Project{Name: "bravo", Image: "img", Env: "{}", Volumes: "{}"}) s.CreateProject(Project{Name: "alpha", Image: "img", Env: "{}", Volumes: "{}"}) projects, err := s.GetAllProjects() if err != nil { t.Fatalf("GetAllProjects: %v", err) } if len(projects) != 2 { t.Fatalf("expected 2 projects, got %d", len(projects)) } // Should be ordered by name if projects[0].Name != "alpha" { t.Fatalf("expected first project 'alpha', got %q", projects[0].Name) } } func TestDeleteProject(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "del-me", Image: "img", Env: "{}", Volumes: "{}"}) err := s.DeleteProject(p.ID) if err != nil { t.Fatalf("DeleteProject: %v", err) } _, err = s.GetProjectByID(p.ID) if err == nil { t.Fatal("expected error getting deleted project") } } func TestCreateAndGetUser(t *testing.T) { s := newTestStore(t) u, err := s.CreateUser(User{ Username: "admin", PasswordHash: "hash123", Role: "admin", }) if err != nil { t.Fatalf("CreateUser: %v", err) } got, err := s.GetUserByUsername("admin") if err != nil { t.Fatalf("GetUserByUsername: %v", err) } if got.ID != u.ID { t.Fatal("user ID mismatch") } if got.Role != "admin" { t.Fatalf("role mismatch: %q", got.Role) } } func TestUserCount(t *testing.T) { s := newTestStore(t) count, err := s.UserCount() if err != nil { t.Fatalf("UserCount: %v", err) } if count != 0 { t.Fatalf("expected 0 users, got %d", count) } s.CreateUser(User{Username: "u1", PasswordHash: "h", Role: "viewer"}) count, _ = s.UserCount() if count != 1 { t.Fatalf("expected 1 user, got %d", count) } } func TestCreateStageAndDeploy(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "proj", Image: "img", Env: "{}", Volumes: "{}"}) stage, err := s.CreateStage(Stage{ ProjectID: p.ID, Name: "dev", TagPattern: "*", MaxInstances: 2, }) if err != nil { t.Fatalf("CreateStage: %v", err) } d, err := s.CreateDeploy(Deploy{ ProjectID: p.ID, StageID: stage.ID, ImageTag: "v1.0", }) if err != nil { t.Fatalf("CreateDeploy: %v", err) } if d.Status != "pending" { t.Fatalf("expected pending status, got %q", d.Status) } err = s.UpdateDeployStatus(d.ID, "success", "") if err != nil { t.Fatalf("UpdateDeployStatus: %v", err) } got, _ := s.GetDeployByID(d.ID) if got.Status != "success" { t.Fatalf("expected success, got %q", got.Status) } } func TestGetDeploysByProjectID(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "proj", Image: "img", Env: "{}", Volumes: "{}"}) stage, _ := s.CreateStage(Stage{ProjectID: p.ID, Name: "dev", TagPattern: "*"}) for i := 0; i < 5; i++ { _, err := s.CreateDeploy(Deploy{ProjectID: p.ID, StageID: stage.ID, ImageTag: "v" + string(rune('0'+i))}) if err != nil { t.Fatalf("CreateDeploy %d: %v", i, err) } } deploys, err := s.GetDeploysByProjectID(p.ID) if err != nil { t.Fatalf("GetDeploysByProjectID: %v", err) } if len(deploys) != 5 { t.Fatalf("expected 5 deploys, got %d", len(deploys)) } } func TestGetRecentDeploys(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "proj", Image: "img", Env: "{}", Volumes: "{}"}) stage, _ := s.CreateStage(Stage{ProjectID: p.ID, Name: "dev", TagPattern: "*"}) for i := 0; i < 5; i++ { s.CreateDeploy(Deploy{ProjectID: p.ID, StageID: stage.ID, ImageTag: "v" + string(rune('0'+i))}) } // Limit to 2 deploys, err := s.GetRecentDeploys(2) if err != nil { t.Fatalf("GetRecentDeploys: %v", err) } if len(deploys) != 2 { t.Fatalf("expected 2 deploys with limit, got %d", len(deploys)) } } func TestDeleteUser(t *testing.T) { s := newTestStore(t) u, _ := s.CreateUser(User{Username: "del-me", PasswordHash: "h", Role: "viewer"}) err := s.DeleteUser(u.ID) if err != nil { t.Fatalf("DeleteUser: %v", err) } _, err = s.GetUserByID(u.ID) if err == nil { t.Fatal("expected error getting deleted user") } } func TestUpdateProject(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "original", Image: "nginx", Env: "{}", Volumes: "{}"}) p.Name = "updated" p.Image = "alpine" err := s.UpdateProject(p) if err != nil { t.Fatalf("UpdateProject: %v", err) } got, _ := s.GetProjectByID(p.ID) if got.Name != "updated" { t.Fatalf("expected name 'updated', got %q", got.Name) } if got.Image != "alpine" { t.Fatalf("expected image 'alpine', got %q", got.Image) } } func TestUpdateUser(t *testing.T) { s := newTestStore(t) u, _ := s.CreateUser(User{Username: "orig", PasswordHash: "h", Role: "viewer"}) u.Username = "renamed" u.Role = "admin" err := s.UpdateUser(u) if err != nil { t.Fatalf("UpdateUser: %v", err) } got, _ := s.GetUserByID(u.ID) if got.Username != "renamed" { t.Fatalf("expected username 'renamed', got %q", got.Username) } if got.Role != "admin" { t.Fatalf("expected role 'admin', got %q", got.Role) } } func TestDeployLogs(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "proj", Image: "img", Env: "{}", Volumes: "{}"}) stage, _ := s.CreateStage(Stage{ProjectID: p.ID, Name: "dev", TagPattern: "*"}) d, _ := s.CreateDeploy(Deploy{ProjectID: p.ID, StageID: stage.ID, ImageTag: "v1"}) err := s.AppendDeployLog(d.ID, "pulling image", "info") if err != nil { t.Fatalf("AppendDeployLog: %v", err) } err = s.AppendDeployLog(d.ID, "something failed", "error") if err != nil { t.Fatalf("AppendDeployLog: %v", err) } logs, err := s.GetDeployLogs(d.ID) if err != nil { t.Fatalf("GetDeployLogs: %v", err) } if len(logs) != 2 { t.Fatalf("expected 2 logs, got %d", len(logs)) } if logs[0].Message != "pulling image" { t.Fatalf("expected first log 'pulling image', got %q", logs[0].Message) } if logs[1].Level != "error" { t.Fatalf("expected second log level 'error', got %q", logs[1].Level) } } func TestGetStagesByProjectID(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "proj", Image: "img", Env: "{}", Volumes: "{}"}) s.CreateStage(Stage{ProjectID: p.ID, Name: "prod", TagPattern: "v*"}) s.CreateStage(Stage{ProjectID: p.ID, Name: "dev", TagPattern: "*"}) stages, err := s.GetStagesByProjectID(p.ID) if err != nil { t.Fatalf("GetStagesByProjectID: %v", err) } if len(stages) != 2 { t.Fatalf("expected 2 stages, got %d", len(stages)) } // Ordered by name if stages[0].Name != "dev" { t.Fatalf("expected first stage 'dev', got %q", stages[0].Name) } } func TestIsTerminalDeployStatus(t *testing.T) { terminals := []string{"success", "failed", "rolled_back"} for _, s := range terminals { if !IsTerminalDeployStatus(s) { t.Fatalf("expected %q to be terminal", s) } } nonTerminals := []string{"pending", "pulling", "starting", "configuring_proxy", "health_checking"} for _, s := range nonTerminals { if IsTerminalDeployStatus(s) { t.Fatalf("expected %q to be non-terminal", s) } } } func TestCascadeDeleteProjectRemovesStagesAndDeploys(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject(Project{Name: "proj", Image: "img", Env: "{}", Volumes: "{}"}) stage, _ := s.CreateStage(Stage{ProjectID: p.ID, Name: "dev", TagPattern: "*"}) s.CreateDeploy(Deploy{ProjectID: p.ID, StageID: stage.ID, ImageTag: "v1"}) err := s.DeleteProject(p.ID) if err != nil { t.Fatalf("DeleteProject: %v", err) } // Stage should be gone _, err = s.GetStageByID(stage.ID) if err == nil { t.Fatal("expected stage to be deleted by cascade") } }