From 32f369d31d7636d84ca64dfe6fa7cfbc4f09c661 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Thu, 28 Nov 2024 10:15:41 +0500 Subject: [PATCH] implement GET endpoint for phase-specific tickets --- db/features.go | 15 +++ db/interface.go | 1 + handlers/features.go | 49 ++++++++++ handlers/features_test.go | 194 ++++++++++++++++++++++++++++++++++++++ mocks/Database.go | 56 +++++++++++ routes/features.go | 1 + 6 files changed, 316 insertions(+) diff --git a/db/features.go b/db/features.go index 4e371a798..cb54f969d 100644 --- a/db/features.go +++ b/db/features.go @@ -327,3 +327,18 @@ func (db database) GetBountiesByPhaseUuid(phaseUuid string) []Bounty { db.db.Model(&Bounty{}).Where("phase_uuid = ?", phaseUuid).Find(&bounties) return bounties } + +func (db database) GetTicketsByPhaseUUID(featureUUID string, phaseUUID string) ([]Tickets, error) { + var tickets []Tickets + + result := db.db. + Where("feature_uuid = ? AND phase_uuid = ?", featureUUID, phaseUUID). + Order("sequence ASC"). + Find(&tickets) + + if result.Error != nil { + return nil, fmt.Errorf("failed to fetch tickets: %w", result.Error) + } + + return tickets, nil +} diff --git a/db/interface.go b/db/interface.go index 62509bcda..0e050fe12 100644 --- a/db/interface.go +++ b/db/interface.go @@ -198,4 +198,5 @@ type Database interface { GetTicket(uuid string) (Tickets, error) UpdateTicket(ticket Tickets) (Tickets, error) DeleteTicket(uuid string) error + GetTicketsByPhaseUUID(featureUUID string, phaseUUID string) ([]Tickets, error) } diff --git a/handlers/features.go b/handlers/features.go index 7ed0c40c6..9c4faa6e5 100644 --- a/handlers/features.go +++ b/handlers/features.go @@ -12,6 +12,7 @@ import ( "os" "github.com/go-chi/chi" + "github.com/google/uuid" "github.com/rs/xid" "github.com/stakwork/sphinx-tribes/auth" "github.com/stakwork/sphinx-tribes/db" @@ -697,3 +698,51 @@ func (oh *featureHandler) BriefSend(w http.ResponseWriter, r *http.Request) { w.WriteHeader(resp.StatusCode) w.Write(respBody) } + +func (oh *featureHandler) GetTicketsByPhaseUUID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + pubKeyFromAuth, _ := ctx.Value(auth.ContextKey).(string) + if pubKeyFromAuth == "" { + fmt.Println("no pubkey from auth") + w.WriteHeader(http.StatusUnauthorized) + return + } + + featureUUID := chi.URLParam(r, "feature_uuid") + phaseUUID := chi.URLParam(r, "phase_uuid") + + if _, err := uuid.Parse(featureUUID); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid feature UUID format"}) + return + } + if _, err := uuid.Parse(phaseUUID); err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid phase UUID format"}) + return + } + + feature := oh.db.GetFeatureByUuid(featureUUID) + if feature.Uuid == "" { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "feature not found"}) + return + } + + _, err := oh.db.GetFeaturePhaseByUuid(featureUUID, phaseUUID) + if err != nil { + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{"error": "Phase not found"}) + return + } + + tickets, err := oh.db.GetTicketsByPhaseUUID(featureUUID, phaseUUID) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(tickets) +} diff --git a/handlers/features_test.go b/handlers/features_test.go index 2e2118f99..d13b55445 100644 --- a/handlers/features_test.go +++ b/handlers/features_test.go @@ -1636,3 +1636,197 @@ func TestGetFeatureStories(t *testing.T) { assert.Equal(t, http.StatusNotAcceptable, rr.Code) }) } + +func TestGetTicketsByPhaseUUID(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + fHandler := NewFeatureHandler(db.TestDB) + + person := db.Person{ + Uuid: uuid.New().String(), + OwnerAlias: "test-alias", + UniqueName: "test-unique-name", + OwnerPubKey: "test-pubkey", + PriceToMeet: 0, + Description: "test-description", + } + db.TestDB.CreateOrEditPerson(person) + + workspace := db.Workspace{ + Uuid: uuid.New().String(), + Name: "test-workspace" + uuid.New().String(), + OwnerPubKey: person.OwnerPubKey, + Github: "https://github.com/test", + Website: "https://www.testwebsite.com", + Description: "test-description", + } + db.TestDB.CreateOrEditWorkspace(workspace) + + feature := db.WorkspaceFeatures{ + Uuid: uuid.New().String(), + WorkspaceUuid: workspace.Uuid, + Name: "test-feature", + Url: "https://github.com/test-feature", + Priority: 0, + } + db.TestDB.CreateOrEditFeature(feature) + + featurePhase := db.FeaturePhase{ + Uuid: uuid.New().String(), + FeatureUuid: feature.Uuid, + Name: "test-phase", + Priority: 0, + } + db.TestDB.CreateOrEditFeaturePhase(featurePhase) + + ticket := db.Tickets{ + UUID: uuid.New(), + FeatureUUID: feature.Uuid, + PhaseUUID: featurePhase.Uuid, + Name: "Test Ticket", + Sequence: 1, + Description: "Test Description", + Status: db.DraftTicket, + } + + db.TestDB.UpdateTicket(ticket) + + ctx := context.WithValue(context.Background(), auth.ContextKey, person.OwnerPubKey) + + t.Run("should return 401 if user is not authorized", func(t *testing.T) { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", feature.Uuid) + rctx.URLParams.Add("phase_uuid", featurePhase.Uuid) + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, + "/features/"+feature.Uuid+"/phase/"+featurePhase.Uuid+"/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("should return 400 if feature UUID is invalid", func(t *testing.T) { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", "invalid-uuid") + rctx.URLParams.Add("phase_uuid", featurePhase.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/features/invalid-uuid/phase/"+featurePhase.Uuid+"/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("should return 400 if phase UUID is invalid", func(t *testing.T) { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", feature.Uuid) + rctx.URLParams.Add("phase_uuid", "invalid-uuid") + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/features/"+feature.Uuid+"/phase/invalid-uuid/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("should return 404 if feature not found", func(t *testing.T) { + nonExistentUUID := uuid.New().String() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", nonExistentUUID) + rctx.URLParams.Add("phase_uuid", featurePhase.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/features/"+nonExistentUUID+"/phase/"+featurePhase.Uuid+"/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + + t.Run("should return 404 if phase not found", func(t *testing.T) { + nonExistentUUID := uuid.New().String() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", feature.Uuid) + rctx.URLParams.Add("phase_uuid", nonExistentUUID) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/features/"+feature.Uuid+"/phase/"+nonExistentUUID+"/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + + t.Run("should return tickets successfully when authorized", func(t *testing.T) { + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", feature.Uuid) + rctx.URLParams.Add("phase_uuid", featurePhase.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/features/"+feature.Uuid+"/phase/"+featurePhase.Uuid+"/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var returnedTickets []db.Tickets + err = json.Unmarshal(rr.Body.Bytes(), &returnedTickets) + assert.NoError(t, err) + assert.Equal(t, 1, len(returnedTickets)) + assert.Equal(t, ticket.Name, returnedTickets[0].Name) + assert.Equal(t, ticket.Description, returnedTickets[0].Description) + assert.Equal(t, ticket.Status, returnedTickets[0].Status) + assert.Equal(t, ticket.Sequence, returnedTickets[0].Sequence) + }) + + t.Run("should return empty array when no tickets exist", func(t *testing.T) { + emptyPhase := db.FeaturePhase{ + Uuid: uuid.New().String(), + FeatureUuid: feature.Uuid, + Name: "empty-phase", + Priority: 1, + } + db.TestDB.CreateOrEditFeaturePhase(emptyPhase) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("feature_uuid", feature.Uuid) + rctx.URLParams.Add("phase_uuid", emptyPhase.Uuid) + req, err := http.NewRequestWithContext(context.WithValue(ctx, chi.RouteCtxKey, rctx), + http.MethodGet, "/features/"+feature.Uuid+"/phase/"+emptyPhase.Uuid+"/tickets", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + http.HandlerFunc(fHandler.GetTicketsByPhaseUUID).ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + + var returnedTickets []db.Tickets + err = json.Unmarshal(rr.Body.Bytes(), &returnedTickets) + assert.NoError(t, err) + assert.Equal(t, 0, len(returnedTickets)) + }) +} diff --git a/mocks/Database.go b/mocks/Database.go index 94be903ce..cc4a1c686 100644 --- a/mocks/Database.go +++ b/mocks/Database.go @@ -9440,3 +9440,59 @@ func NewDatabase(t interface { return mock } + +// GetTicketsByPhaseUUID provides a mock function with given fields: featureUUID, phaseUUID +func (_m *Database) GetTicketsByPhaseUUID(featureUUID string, phaseUUID string) ([]db.Tickets, error) { + ret := _m.Called(featureUUID, phaseUUID) + + if len(ret) == 0 { + panic("no return value specified for GetTicketsByPhaseUUID") + } + + var r0 []db.Tickets + var r1 error + if rf, ok := ret.Get(0).(func(string, string) ([]db.Tickets, error)); ok { + return rf(featureUUID, phaseUUID) + } + if rf, ok := ret.Get(0).(func(string, string) []db.Tickets); ok { + r0 = rf(featureUUID, phaseUUID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]db.Tickets) + } + } + + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(featureUUID, phaseUUID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type Database_GetTicketsByPhaseUUID_Call struct { + *mock.Call +} + + +func (_e *Database_Expecter) GetTicketsByPhaseUUID(featureUUID interface{}, phaseUUID interface{}) *Database_GetTicketsByPhaseUUID_Call { + return &Database_GetTicketsByPhaseUUID_Call{Call: _e.mock.On("GetTicketsByPhaseUUID", featureUUID, phaseUUID)} +} + +func (_c *Database_GetTicketsByPhaseUUID_Call) Run(run func(featureUUID string, phaseUUID string)) *Database_GetTicketsByPhaseUUID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(string), args[1].(string)) + }) + return _c +} + +func (_c *Database_GetTicketsByPhaseUUID_Call) Return(_a0 []db.Tickets, _a1 error) *Database_GetTicketsByPhaseUUID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Database_GetTicketsByPhaseUUID_Call) RunAndReturn(run func(string, string) ([]db.Tickets, error)) *Database_GetTicketsByPhaseUUID_Call { + _c.Call.Return(run) + return _c +} \ No newline at end of file diff --git a/routes/features.go b/routes/features.go index a3223014d..ab8b426c1 100644 --- a/routes/features.go +++ b/routes/features.go @@ -39,6 +39,7 @@ func FeatureRoutes() chi.Router { r.Get("/{feature_uuid}/story/{story_uuid}", featureHandlers.GetStoryByUuid) r.Delete("/{feature_uuid}/story/{story_uuid}", featureHandlers.DeleteStory) r.Get("/{feature_uuid}/phase/{phase_uuid}/bounty", featureHandlers.GetBountiesByFeatureAndPhaseUuid) + r.Get("/{feature_uuid}/phase/{phase_uuid}/tickets", featureHandlers.GetTicketsByPhaseUUID) r.Get("/{feature_uuid}/phase/{phase_uuid}/bounty/count", featureHandlers.GetBountiesCountByFeatureAndPhaseUuid) })