diff --git a/pkg/api/handlers/handler_utils.go b/pkg/api/handlers/handler_utils.go index 2fe9fdc..e796df9 100644 --- a/pkg/api/handlers/handler_utils.go +++ b/pkg/api/handlers/handler_utils.go @@ -52,15 +52,16 @@ func ParseTimeFromStringWithDefault(timeString string, defaultTime time.Time) (t func GetProjectSpecStatistics(h *Handler, projectId string) []models.TestSummary { var testSummaries []models.TestSummary + h.db.Table("test_runs"). Joins("INNER JOIN suite_runs ON test_runs.id = suite_runs.test_run_id"). Joins("INNER JOIN spec_runs ON suite_runs.id = spec_runs.suite_id"). - Select(`suite_runs.id AS suite_run_id, - suite_runs.suite_name, - test_runs.start_time, - COUNT(spec_runs.id) FILTER (WHERE spec_runs.status = 'passed') AS total_passed_spec_runs, - COUNT(spec_runs.id) FILTER (WHERE spec_runs.status = 'skipped') AS total_skipped_spec_runs, - COUNT(spec_runs.id) AS total_spec_runs`). + Select(`suite_runs.id AS suite_run_id, + suite_runs.suite_name, + test_runs.start_time, + COUNT(spec_runs.id) FILTER (WHERE LOWER(spec_runs.status) = 'passed') AS total_passed_spec_runs, + COUNT(spec_runs.id) FILTER (WHERE LOWER(spec_runs.status) = 'skipped') AS total_skipped_spec_runs, + COUNT(spec_runs.id) AS total_spec_runs`). Where("test_runs.project_id = ?", projectId). Group("suite_runs.id, test_runs.start_time"). Order("test_runs.start_time"). diff --git a/pkg/api/handlers/handlers.go b/pkg/api/handlers/handlers.go index 79393c0..12ef1a9 100644 --- a/pkg/api/handlers/handlers.go +++ b/pkg/api/handlers/handlers.go @@ -3,6 +3,8 @@ package handlers import ( "errors" "fmt" + "sort" + "strings" "github.com/guidewire/fern-reporter/config" "github.com/guidewire/fern-reporter/pkg/models" @@ -21,6 +23,29 @@ type Handler struct { db *gorm.DB } +type ProjectSummary struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Status models.TestRunStatus `json:"status"` + TestCount uint64 `json:"test_count"` + TestPassed uint64 `json:"test_passed"` + TestFailed uint64 `json:"test_failed"` + TestSkipped uint64 `json:"test_skipped"` + Date time.Time `json:"date"` + GitBranch string `json:"git_branch"` +} + +type ProjectGroup struct { + GroupID uint64 `json:"group_id"` + GroupName string `json:"group_name"` + Projects []ProjectSummary `json:"projects"` +} + +type ProjectGroupResponse struct { + Cookie string `json:"cookie"` + ProjectGroups []ProjectGroup `json:"project_groups"` +} + func NewHandler(db *gorm.DB) *Handler { return &Handler{db: db} } @@ -67,6 +92,8 @@ func (h *Handler) CreateTestRun(c *gin.Context) { return // Stop further processing if tag processing fails } + computeTestRunStatus(&testRun) + // Save or update the testRun record in the database if err := gdb.Save(&testRun).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "error saving record"}) @@ -83,6 +110,24 @@ func getProjectIDByUUID(db *gorm.DB, uuid string) (uint64, error) { return project.ID, nil } +func computeTestRunStatus(testRun *models.TestRun) { + status := models.StatusPassed + + for _, suite := range testRun.SuiteRuns { + for _, spec := range suite.SpecRuns { + if strings.EqualFold(spec.Status, "FAILED") { + testRun.Status = models.StatusFailed + return + } + if strings.EqualFold(spec.Status, "SKIPPED") { + status = models.StatusSkipped + } + } + } + + testRun.Status = status +} + func ProcessTags(db *gorm.DB, testRun *models.TestRun) error { for i, suite := range testRun.SuiteRuns { for j, spec := range suite.SpecRuns { @@ -267,6 +312,174 @@ func (h *Handler) GetTestSummary(c *gin.Context) { c.JSON(http.StatusOK, testSummaries) } +func (h *Handler) GetProjectGroups(c *gin.Context) { + ucookie, _ := c.Cookie(utils.CookieName) + + // 1. Get the user + var user models.AppUser + if err := h.db.Where("cookie = ?", ucookie).First(&user).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "error fetching user"}) + } + return + } + + groupIDStr := c.Query("group_id") + branch := c.Query("git_branch") + + // 2. Get preferred projects + var preferred []models.PreferredProject + query := h.db.Preload("Project"). + Preload("Group"). + Where("user_id = ?", user.ID) + + if groupIDStr != "" { + if groupID, err := strconv.ParseUint(groupIDStr, 10, 64); err == nil { + query = query.Where("group_id = ?", groupID) + } + } + + err := query.Find(&preferred).Error + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "error fetching preferences"}) + return + } + + // 3. Get project summaries (filtering by branch if given) + projectSummaryMap, err := h.getProjectSummaryMapping(preferred, branch) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 4. Group by group ID + groupMap := make(map[uint64]*ProjectGroup) + for _, item := range preferred { + if item.Group == nil { + continue // skip ungrouped + } + + groupID := item.Group.GroupID + if _, exists := groupMap[groupID]; !exists { + groupMap[groupID] = &ProjectGroup{ + GroupID: groupID, + GroupName: item.Group.GroupName, + Projects: []ProjectSummary{}, + } + } + + if summary, ok := projectSummaryMap[item.Project.UUID]; ok { + groupMap[groupID].Projects = append(groupMap[groupID].Projects, summary) + } + } + + // 5. Convert map to slice + var grouped []ProjectGroup + for _, group := range groupMap { + grouped = append(grouped, *group) + } + + sort.Slice(grouped, func(i, j int) bool { + return grouped[i].GroupID < grouped[j].GroupID + }) + + // 6. Return response + response := ProjectGroupResponse{ + Cookie: user.Cookie, + ProjectGroups: grouped, + } + + c.JSON(http.StatusOK, response) +} + +func (h *Handler) getProjectSummaryMapping(preferred []models.PreferredProject, branchFilter string) (map[string]ProjectSummary, error) { + projectSummaryMap := make(map[string]ProjectSummary) + + for _, pref := range preferred { + var latestTestSeed int64 + + // Step 1: Fetch only latest test_seed (no full TestRun) + query := h.db.Model(&models.TestRun{}). + Select("test_seed"). + Joins("JOIN project_details ON project_details.id = test_runs.project_id"). + Where("project_details.uuid = ?", pref.Project.UUID) + + if branchFilter != "" { + query = query.Where("test_runs.git_branch = ?", branchFilter) + } + + err := query.Order("test_seed DESC"). + Limit(1). + Pluck("test_seed", &latestTestSeed).Error + + if err != nil { + return nil, fmt.Errorf("failed to fetch latest test_seed for project %s: %w", pref.Project.UUID, err) + } + + if latestTestSeed == 0 { + continue // No test runs for this project + } + + // Step 2: Aggregate test status summary for this test_seed + var stats struct { + TestPassed uint64 + TestSkipped uint64 + TestFailed uint64 + TestCount uint64 + GitBranch string + } + + err = h.db.Model(&models.TestRun{}). + Select([]string{ + "SUM(CASE WHEN UPPER(spec_runs.status) = 'PASSED' THEN 1 ELSE 0 END) AS test_passed", + "SUM(CASE WHEN UPPER(spec_runs.status) = 'SKIPPED' THEN 1 ELSE 0 END) AS test_skipped", + "SUM(CASE WHEN UPPER(spec_runs.status) = 'FAILED' THEN 1 ELSE 0 END) AS test_failed", + "COUNT(*) AS test_count", + "MAX(test_runs.git_branch) AS git_branch", + }). + Joins("JOIN suite_runs ON suite_runs.test_run_id = test_runs.id"). + Joins("JOIN spec_runs ON spec_runs.suite_id = suite_runs.id"). + Joins("JOIN project_details ON project_details.id = test_runs.project_id"). + Where("project_details.uuid = ? AND test_runs.test_seed = ?", pref.Project.UUID, latestTestSeed). + Scan(&stats).Error + + if err != nil { + return nil, fmt.Errorf("aggregation failed for project %s: %w", pref.Project.UUID, err) + } + + projectSummary := ProjectSummary{ + UUID: pref.Project.UUID, + Name: pref.Project.Name, + Date: time.Unix(latestTestSeed, 0), + GitBranch: stats.GitBranch, + TestPassed: stats.TestPassed, + TestSkipped: stats.TestSkipped, + TestFailed: stats.TestFailed, + TestCount: stats.TestCount, + } + + projectSummary.Status = computeProjectSummaryStatus(stats.TestFailed, stats.TestSkipped) + + // Step 3: Create project summary entry + projectSummaryMap[pref.Project.UUID] = projectSummary + } + + return projectSummaryMap, nil +} + +func computeProjectSummaryStatus(failed uint64, skipped uint64) models.TestRunStatus { + if failed > 0 { + return models.StatusFailed + } else if skipped > 0 { + return models.StatusSkipped + } else { + return models.StatusPassed + } +} + func (h *Handler) Ping(c *gin.Context) { c.JSON(200, gin.H{ "message": "Fern Reporter is running!", diff --git a/pkg/api/handlers/handlers_test.go b/pkg/api/handlers/handlers_test.go index 9bb6736..671704d 100644 --- a/pkg/api/handlers/handlers_test.go +++ b/pkg/api/handlers/handlers_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/guidewire/fern-reporter/pkg/utils" "io" "net/http" "net/http/httptest" @@ -125,6 +126,7 @@ var _ = Describe("Handlers", func() { BuildTriggerActor: "Actor Name", BuildUrl: "https://someurl.com", TestSeed: 0, + Status: "PASSED", SuiteRuns: []models.SuiteRun{ { ID: 1, @@ -161,8 +163,8 @@ var _ = Describe("Handlers", func() { WillReturnRows(rows) mock.ExpectBegin() - mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "test_runs" ("test_project_name","project_id","test_seed","start_time","end_time","git_branch","git_sha","build_trigger_actor","build_url") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id"`)). - WithArgs(expectedTestRun.TestProjectName, expectedProject.ID, expectedTestRun.TestSeed, expectedTestRun.StartTime, expectedTestRun.EndTime, expectedTestRun.GitBranch, expectedTestRun.GitSha, expectedTestRun.BuildTriggerActor, expectedTestRun.BuildUrl). + mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "test_runs" ("test_project_name","project_id","test_seed","start_time","end_time","git_branch","git_sha","build_trigger_actor","build_url","status") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) RETURNING "id"`)). + WithArgs(expectedTestRun.TestProjectName, expectedProject.ID, expectedTestRun.TestSeed, expectedTestRun.StartTime, expectedTestRun.EndTime, expectedTestRun.GitBranch, expectedTestRun.GitSha, expectedTestRun.BuildTriggerActor, expectedTestRun.BuildUrl, expectedTestRun.Status). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) mock.ExpectCommit() @@ -1064,8 +1066,8 @@ var _ = Describe("Handlers", func() { mock.ExpectQuery(regexp.QuoteMeta(`SELECT suite_runs.id AS suite_run_id, suite_runs.suite_name, test_runs.start_time, - COUNT(spec_runs.id) FILTER (WHERE spec_runs.status = 'passed') AS total_passed_spec_runs, - COUNT(spec_runs.id) FILTER (WHERE spec_runs.status = 'skipped') AS total_skipped_spec_runs, + COUNT(spec_runs.id) FILTER (WHERE LOWER(spec_runs.status) = 'passed') AS total_passed_spec_runs, + COUNT(spec_runs.id) FILTER (WHERE LOWER(spec_runs.status) = 'skipped') AS total_skipped_spec_runs, COUNT(spec_runs.id) AS total_spec_runs FROM "test_runs" INNER JOIN suite_runs ON test_runs.id = suite_runs.test_run_id INNER JOIN spec_runs ON suite_runs.id = spec_runs.suite_id @@ -1102,4 +1104,230 @@ var _ = Describe("Handlers", func() { Expect(w.Body.String()).To(MatchJSON(expectedJSON)) }) }) + + Context("when get project groups is invoked", func() { + projectId := "96ad860-2a9a-504f-8861-aeafd0b2ae29" + ucookie := "5c0fc06d-26d9-4202-a1f3-2d024e957171" + + It("will return project group details", func() { + + reqBody, err := json.Marshal("") + Expect(err).ToNot(HaveOccurred()) + + user_rows := sqlmock.NewRows([]string{"id", "is_dark", "timezone", "cookie"}). + AddRow(1, true, "America/New_York", ucookie) + + project_group_rows := sqlmock.NewRows([]string{"group_id", "user_id", "group_name"}). + AddRow(1, 1, "First Group") + + project_rows := sqlmock.NewRows([]string{"id", "uuid", "name"}). + AddRow(1, projectId, "First Project"). + AddRow(2, "59e06cf8-f390-5093-af2e-3685be593a25", "Second Project") + + preferred_projects := sqlmock.NewRows([]string{"id", "user_id", "project_id", "group_id"}). + AddRow(1, 1, 1, 1). + AddRow(2, 1, 2, 1) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "app_users" WHERE cookie = $1 ORDER BY "app_users"."id" LIMIT $2`)). + WithArgs(ucookie, 1). + WillReturnRows(user_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "preferred_projects" WHERE user_id = $1`)). + WithArgs(1). + WillReturnRows(preferred_projects) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "project_groups" WHERE "project_groups"."group_id" = $1`)). + WithArgs(1). + WillReturnRows(project_group_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "project_details" WHERE "project_details"."id" IN ($1,$2)`)). + WithArgs(1, 2). + WillReturnRows(project_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "test_seed" FROM "test_runs" JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 ORDER BY test_seed DESC LIMIT $2`)). + WithArgs("96ad860-2a9a-504f-8861-aeafd0b2ae29", 1). + WillReturnRows(sqlmock.NewRows([]string{"test_seed"}).AddRow(123456)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT SUM(CASE WHEN UPPER(spec_runs.status) = 'PASSED' THEN 1 ELSE 0 END) AS test_passed,SUM(CASE WHEN UPPER(spec_runs.status) = 'SKIPPED' THEN 1 ELSE 0 END) AS test_skipped,SUM(CASE WHEN UPPER(spec_runs.status) = 'FAILED' THEN 1 ELSE 0 END) AS test_failed,COUNT(*) AS test_count,MAX(test_runs.git_branch) AS git_branch FROM "test_runs" JOIN suite_runs ON suite_runs.test_run_id = test_runs.id JOIN spec_runs ON spec_runs.suite_id = suite_runs.id JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 AND test_runs.test_seed = $2 `)). + WithArgs("96ad860-2a9a-504f-8861-aeafd0b2ae29", 123456). + WillReturnRows(sqlmock.NewRows([]string{ + "test_passed", "test_skipped", "test_failed", "test_count", "git_branch", + }).AddRow(4, 2, 1, 7, "main")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "test_seed" FROM "test_runs" JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 ORDER BY test_seed DESC LIMIT $2`)). + WithArgs("59e06cf8-f390-5093-af2e-3685be593a25", 1). + WillReturnRows(sqlmock.NewRows([]string{"test_seed"}).AddRow(234567)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT SUM(CASE WHEN UPPER(spec_runs.status) = 'PASSED' THEN 1 ELSE 0 END) AS test_passed,SUM(CASE WHEN UPPER(spec_runs.status) = 'SKIPPED' THEN 1 ELSE 0 END) AS test_skipped,SUM(CASE WHEN UPPER(spec_runs.status) = 'FAILED' THEN 1 ELSE 0 END) AS test_failed,COUNT(*) AS test_count,MAX(test_runs.git_branch) AS git_branch FROM "test_runs" JOIN suite_runs ON suite_runs.test_run_id = test_runs.id JOIN spec_runs ON spec_runs.suite_id = suite_runs.id JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 AND test_runs.test_seed = $2 `)). + WithArgs("59e06cf8-f390-5093-af2e-3685be593a25", 234567). + WillReturnRows(sqlmock.NewRows([]string{ + "test_passed", "test_skipped", "test_failed", "test_count", "git_branch", + }).AddRow(2, 2, 1, 5, "feature/payment")) + + // Create request + req := httptest.NewRequest(http.MethodGet, "/api/testrun/project-groups", bytes.NewBuffer([]byte(reqBody))) + req.Header.Set("Content-Type", "application/json") + + // Set the cookie on the request + req.AddCookie(&http.Cookie{ + Name: utils.CookieName, + Value: ucookie, + }) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + handler := handlers.NewHandler(gormDb) + handler.GetProjectGroups(c) + + var responseBody handlers.ProjectGroupResponse + err = json.Unmarshal(w.Body.Bytes(), &responseBody) + Expect(err).ToNot(HaveOccurred()) + + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(responseBody.ProjectGroups).To(Not(BeEmpty())) + Expect(responseBody.ProjectGroups[0].GroupName).To(Equal("First Group")) + Expect(len(responseBody.ProjectGroups[0].Projects)).To(Equal(2)) + Expect(responseBody.ProjectGroups[0].Projects[0].UUID).To(Equal("96ad860-2a9a-504f-8861-aeafd0b2ae29")) + Expect(responseBody.ProjectGroups[0].Projects[1].UUID).To(Equal("59e06cf8-f390-5093-af2e-3685be593a25")) + }) + + It("will return project group details filtered by group_id and git_branch", func() { + + reqBody, err := json.Marshal("") + Expect(err).ToNot(HaveOccurred()) + + user_rows := sqlmock.NewRows([]string{"id", "is_dark", "timezone", "cookie"}). + AddRow(1, true, "America/New_York", ucookie) + + project_group_rows := sqlmock.NewRows([]string{"group_id", "user_id", "group_name"}). + AddRow(1, 1, "First Group") + + project_rows := sqlmock.NewRows([]string{"id", "uuid", "name"}). + AddRow(1, projectId, "First Project"). + AddRow(2, "59e06cf8-f390-5093-af2e-3685be593a25", "Second Project") + + preferred_projects := sqlmock.NewRows([]string{"id", "user_id", "project_id", "group_id"}). + AddRow(1, 1, 1, 1). + AddRow(2, 1, 2, 1) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "app_users" WHERE cookie = $1 ORDER BY "app_users"."id" LIMIT $2`)). + WithArgs(ucookie, 1). + WillReturnRows(user_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "preferred_projects" WHERE user_id = $1 AND group_id = $2`)). + WithArgs(1, 1). + WillReturnRows(preferred_projects) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "project_groups" WHERE "project_groups"."group_id" = $1`)). + WithArgs(1). + WillReturnRows(project_group_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "project_details" WHERE "project_details"."id" IN ($1,$2)`)). + WithArgs(1, 2). + WillReturnRows(project_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "test_seed" FROM "test_runs" JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 AND test_runs.git_branch = $2 ORDER BY test_seed DESC LIMIT $3`)). + WithArgs("96ad860-2a9a-504f-8861-aeafd0b2ae29", "main", 1). + WillReturnRows(sqlmock.NewRows([]string{"test_seed"}).AddRow(123456)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT SUM(CASE WHEN UPPER(spec_runs.status) = 'PASSED' THEN 1 ELSE 0 END) AS test_passed,SUM(CASE WHEN UPPER(spec_runs.status) = 'SKIPPED' THEN 1 ELSE 0 END) AS test_skipped,SUM(CASE WHEN UPPER(spec_runs.status) = 'FAILED' THEN 1 ELSE 0 END) AS test_failed,COUNT(*) AS test_count,MAX(test_runs.git_branch) AS git_branch FROM "test_runs" JOIN suite_runs ON suite_runs.test_run_id = test_runs.id JOIN spec_runs ON spec_runs.suite_id = suite_runs.id JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 AND test_runs.test_seed = $2 `)). + WithArgs("96ad860-2a9a-504f-8861-aeafd0b2ae29", 123456). + WillReturnRows(sqlmock.NewRows([]string{ + "test_passed", "test_skipped", "test_failed", "test_count", "git_branch", + }).AddRow(4, 2, 1, 7, "main")) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT "test_seed" FROM "test_runs" JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 AND test_runs.git_branch = $2 ORDER BY test_seed DESC LIMIT $3`)). + WithArgs("59e06cf8-f390-5093-af2e-3685be593a25", "main", 1). + WillReturnRows(sqlmock.NewRows([]string{"test_seed"}).AddRow(234567)) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT SUM(CASE WHEN UPPER(spec_runs.status) = 'PASSED' THEN 1 ELSE 0 END) AS test_passed,SUM(CASE WHEN UPPER(spec_runs.status) = 'SKIPPED' THEN 1 ELSE 0 END) AS test_skipped,SUM(CASE WHEN UPPER(spec_runs.status) = 'FAILED' THEN 1 ELSE 0 END) AS test_failed,COUNT(*) AS test_count,MAX(test_runs.git_branch) AS git_branch FROM "test_runs" JOIN suite_runs ON suite_runs.test_run_id = test_runs.id JOIN spec_runs ON spec_runs.suite_id = suite_runs.id JOIN project_details ON project_details.id = test_runs.project_id WHERE project_details.uuid = $1 AND test_runs.test_seed = $2 `)). + WithArgs("59e06cf8-f390-5093-af2e-3685be593a25", 234567). + WillReturnRows(sqlmock.NewRows([]string{ + "test_passed", "test_skipped", "test_failed", "test_count", "git_branch", + }).AddRow(2, 2, 1, 5, "main")) + + // Create request + req := httptest.NewRequest(http.MethodGet, "/api/testrun/project-groups?group_id=1&git_branch=main", bytes.NewBuffer([]byte(reqBody))) + req.Header.Set("Content-Type", "application/json") + + // Set the cookie on the request + req.AddCookie(&http.Cookie{ + Name: utils.CookieName, + Value: ucookie, + }) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + handler := handlers.NewHandler(gormDb) + handler.GetProjectGroups(c) + + var responseBody handlers.ProjectGroupResponse + err = json.Unmarshal(w.Body.Bytes(), &responseBody) + Expect(err).ToNot(HaveOccurred()) + + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(responseBody.ProjectGroups).To(Not(BeEmpty())) + Expect(responseBody.ProjectGroups[0].GroupName).To(Equal("First Group")) + Expect(len(responseBody.ProjectGroups[0].Projects)).To(Equal(2)) + Expect(responseBody.ProjectGroups[0].Projects[0].UUID).To(Equal("96ad860-2a9a-504f-8861-aeafd0b2ae29")) + Expect(responseBody.ProjectGroups[0].Projects[1].UUID).To(Equal("59e06cf8-f390-5093-af2e-3685be593a25")) + }) + + It("for empty project groups details, will return empty object", func() { + reqBody, err := json.Marshal("") + if err != nil { + fmt.Printf("Error serializing SuiteRuns: %v", err) + return + } + + user_rows := sqlmock.NewRows([]string{"ID", "IsDark", "Timezone", "Cookie"}). + AddRow(1, true, "America/New_York", ucookie) + + project_rows := sqlmock.NewRows([]string{"ID", "UUID", "Name"}). + AddRow(1, projectId, "First Project"). + AddRow(2, "59e06cf8-f390-5093-af2e-3685be593a25", "Second Project") + + preferred_projects := sqlmock.NewRows([]string{"ID", "UserID", "ProjectID", "GroupID"}) //empty rows + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "app_users" WHERE cookie = $1 ORDER BY "app_users"."id" LIMIT $2`)). + WithArgs(ucookie, 1). + WillReturnRows(user_rows) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "preferred_projects" WHERE user_id = $1`)). + WithArgs(1). + WillReturnRows(preferred_projects) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "project_details" WHERE "project_details"."id" IN ($1,$2)`)). + WithArgs(1, 2). + WillReturnRows(project_rows) + + // Create request + req := httptest.NewRequest(http.MethodDelete, "/api/user/preference", bytes.NewBuffer([]byte(reqBody))) + req.Header.Set("Content-Type", "application/json") + + // Set the cookie on the request + req.AddCookie(&http.Cookie{ + Name: utils.CookieName, + Value: ucookie, + }) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = req + + handler := handlers.NewHandler(gormDb) + handler.GetProjectGroups(c) + + var responseBody handlers.ProjectGroupResponse + err = json.Unmarshal(w.Body.Bytes(), &responseBody) + + Expect(err).ToNot(HaveOccurred()) + Expect(w.Code).To(Equal(http.StatusOK)) + Expect(responseBody.ProjectGroups).To(BeNil()) + }) + }) }) diff --git a/pkg/api/handlers/user/user_preference.go b/pkg/api/handlers/user/user_preference.go index 9a62e8d..e5e98ce 100644 --- a/pkg/api/handlers/user/user_preference.go +++ b/pkg/api/handlers/user/user_preference.go @@ -142,27 +142,27 @@ func (h *UserHandler) DeleteFavouriteProject(c *gin.Context) { } func (h *UserHandler) GetFavouriteProject(c *gin.Context) { - ucookie, _ := c.Cookie(utils.CookieName) - - user, err := GetUserObject(h, ucookie) - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("User ID not found: %v", err)}) - return - } - - var uuids []string - err = h.db. - Table("preferred_projects"). - Joins("JOIN project_details ON preferred_projects.project_id = project_details.id"). - Where("preferred_projects.user_id = ? AND preferred_projects.group_id IS NULL", user.ID). - Pluck("project_details.uuid", &uuids).Error - - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": "error fetching favourite project uuids"}) - return - } - - c.JSON(http.StatusOK, uuids) + ucookie, _ := c.Cookie(utils.CookieName) + + user, err := GetUserObject(h, ucookie) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("User ID not found: %v", err)}) + return + } + + var uuids []string + err = h.db. + Table("preferred_projects"). + Joins("JOIN project_details ON preferred_projects.project_id = project_details.id"). + Where("preferred_projects.user_id = ? AND preferred_projects.group_id IS NULL", user.ID). + Pluck("project_details.uuid", &uuids).Error + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "error fetching favourite project uuids"}) + return + } + + c.JSON(http.StatusOK, uuids) } func (h *UserHandler) SaveUserPreference(c *gin.Context) { diff --git a/pkg/api/routers/routers.go b/pkg/api/routers/routers.go index b11370b..c4dc654 100644 --- a/pkg/api/routers/routers.go +++ b/pkg/api/routers/routers.go @@ -48,6 +48,7 @@ func RegisterRouters(router *gin.Engine) { testRun.POST("/", handler.CreateTestRun) testRun.PUT("/:id", handler.UpdateTestRun) testRun.DELETE("/:id", handler.DeleteTestRun) + testRun.GET("/project-groups", handler.GetProjectGroups) testReport := api.Group("/reports") testReport.GET("/projects/", projectHandler.GetAllProjectsForReport) diff --git a/pkg/api/routers/routers_test.go b/pkg/api/routers/routers_test.go index 564065f..f825bee 100644 --- a/pkg/api/routers/routers_test.go +++ b/pkg/api/routers/routers_test.go @@ -63,6 +63,7 @@ var _ = Describe("RegisterRouters", func() { ExpectRoute(router, "POST", "/api/testrun/", handler.CreateTestRun) ExpectRoute(router, "PUT", "/api/testrun/:id", handler.UpdateTestRun) ExpectRoute(router, "DELETE", "/api/testrun/:id", handler.DeleteTestRun) + ExpectRoute(router, "GET", "/api/testrun/project-groups", handler.GetProjectGroups) ExpectRoute(router, "GET", "/api/project", projectHandler.GetAllProjects) ExpectRoute(router, "POST", "/api/project", projectHandler.CreateProject) @@ -71,7 +72,6 @@ var _ = Describe("RegisterRouters", func() { ExpectRoute(router, "POST", "/api/user/favourite", userHandler.SaveFavouriteProject) ExpectRoute(router, "DELETE", "/api/user/favourite/:projectUUID", userHandler.DeleteFavouriteProject) - ExpectRoute(router, "GET", "/api/user/favourite", userHandler.GetFavouriteProject) ExpectRoute(router, "PUT", "/api/user/preference", userHandler.SaveUserPreference) ExpectRoute(router, "GET", "/api/user/preference", userHandler.GetUserPreference) ExpectRoute(router, "POST", "/api/user/preferred", userHandler.SavePreferredProject) diff --git a/pkg/db/migrations/000009_alter_test_runs_table.down.sql b/pkg/db/migrations/000009_alter_test_runs_table.down.sql new file mode 100644 index 0000000..ddf6a7a --- /dev/null +++ b/pkg/db/migrations/000009_alter_test_runs_table.down.sql @@ -0,0 +1,6 @@ +-- Step 1: Remove the column +ALTER TABLE public.test_runs +DROP COLUMN status; + +-- Step 2: Drop the ENUM type +DROP TYPE test_run_status; \ No newline at end of file diff --git a/pkg/db/migrations/000009_alter_test_runs_table.up.sql b/pkg/db/migrations/000009_alter_test_runs_table.up.sql new file mode 100644 index 0000000..c163ee4 --- /dev/null +++ b/pkg/db/migrations/000009_alter_test_runs_table.up.sql @@ -0,0 +1,6 @@ +-- Step 1: Create the ENUM type +CREATE TYPE test_run_status AS ENUM ('FAILED', 'SKIPPED', 'PASSED'); + +-- Step 2: Add a new column (no TYPE keyword needed) +ALTER TABLE public.test_runs +ADD COLUMN status test_run_status; \ No newline at end of file diff --git a/pkg/models/types.go b/pkg/models/types.go index 2093403..02df71f 100644 --- a/pkg/models/types.go +++ b/pkg/models/types.go @@ -9,19 +9,28 @@ type TimeLog struct { EndTime time.Time `json:"end_time"` } +type TestRunStatus string + +const ( + StatusFailed TestRunStatus = "FAILED" + StatusSkipped TestRunStatus = "SKIPPED" + StatusPassed TestRunStatus = "PASSED" +) + type TestRun struct { - ID uint64 `json:"id" gorm:"primaryKey"` - TestProjectName string `json:"test_project_name"` - TestProjectID string `json:"test_project_id" gorm:"-"` - ProjectID uint64 `json:"project_id" gorm:"column:project_id"` // Foreign key - TestSeed uint64 `json:"test_seed"` - StartTime time.Time `json:"start_time"` - EndTime time.Time `json:"end_time"` - GitBranch string `json:"git_branch"` - GitSha string `json:"git_sha"` - BuildTriggerActor string `json:"build_trigger_actor"` - BuildUrl string `json:"build_url"` - SuiteRuns []SuiteRun `json:"suite_runs" gorm:"foreignKey:TestRunID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + ID uint64 `json:"id" gorm:"primaryKey"` + TestProjectName string `json:"test_project_name"` + TestProjectID string `json:"test_project_id" gorm:"-"` + ProjectID uint64 `json:"project_id" gorm:"column:project_id"` // Foreign key + TestSeed uint64 `json:"test_seed"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` + GitBranch string `json:"git_branch"` + GitSha string `json:"git_sha"` + BuildTriggerActor string `json:"build_trigger_actor"` + BuildUrl string `json:"build_url"` + SuiteRuns []SuiteRun `json:"suite_runs" gorm:"foreignKey:TestRunID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` + Status TestRunStatus `gorm:"type:test_run_status"` // Relationship with ProjectDetails Project ProjectDetails `json:"project" gorm:"foreignKey:ProjectID;references:ID"`