Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Phase Tickets API: Route, Handler, and Utility Function #1994

Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions db/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,17 @@ func (db database) GetBountiesByPhaseUuid(phaseUuid string) []Bounty {
db.db.Model(&Bounty{}).Where("phase_uuid = ?", phaseUuid).Find(&bounties)
return bounties
}

func (db database) GetTicketsByPhase(featureUuid string, phaseUuid string) ([]Tickets, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MuhammadUmer44 we are to get tickets by phaseUuid alone not by phaseUuid and featureUuid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no concept currently where we would have phases without a feature so this logic is fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MuhammadUmer44 can you resolve this one so we can get it merged?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@humansinstitute, Sure, I am on it.

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 for phase: %w", result.Error)
}

return tickets, nil
}
1 change: 1 addition & 0 deletions db/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,5 @@ type Database interface {
GetTicket(uuid string) (Tickets, error)
UpdateTicket(ticket Tickets) (Tickets, error)
DeleteTicket(uuid string) error
GetTicketsByPhase(featureUuid string, phaseUuid string) ([]Tickets, error)
}
43 changes: 43 additions & 0 deletions handlers/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -697,3 +698,45 @@ func (oh *featureHandler) BriefSend(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(resp.StatusCode)
w.Write(respBody)
}

func (oh *featureHandler) GetTickets(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
}

_, err := oh.db.GetFeaturePhaseByUuid(featureUuid, phaseUuid)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You suppose to use this new function GetTicketsByPhase you just created here and not this function GetFeaturePhaseByUuid

if err != nil {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(map[string]string{"error": "Phase not found"})
return
}

tickets, err := oh.db.GetTicketsByPhase(featureUuid, phaseUuid)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also based on the issue related to this, you are suppose to return only the ticketUuids and not the entire ticket object

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this should have been updated and the approach is corrrect.

Initially I had a design to return uuids and then use a different operation to get all details.
But it makes more sense to just return the whole object in one go whilst we're there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a situation where the description in the ticket was correct but I'd not updated the title which still referenced UUIDs (updated)

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)
}
161 changes: 161 additions & 0 deletions handlers/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1636,3 +1636,164 @@ func TestGetFeatureStories(t *testing.T) {
assert.Equal(t, http.StatusNotAcceptable, rr.Code)
})
}

func TestGetTickets(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.GetTickets).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.GetTickets).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.GetTickets).ServeHTTP(rr, req)

assert.Equal(t, http.StatusBadRequest, 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.GetTickets).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.GetTickets).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))
})
}
55 changes: 55 additions & 0 deletions mocks/Database.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions routes/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func FeatureRoutes() chi.Router {
r.Post("/phase", featureHandlers.CreateOrEditFeaturePhase)
r.Get("/{feature_uuid}/phase", featureHandlers.GetFeaturePhases)
r.Get("/{feature_uuid}/phase/{phase_uuid}", featureHandlers.GetFeaturePhaseByUUID)
r.Get("/{feature_uuid}/phase/{phase_uuid}/tickets", featureHandlers.GetTickets)
r.Delete("/{feature_uuid}/phase/{phase_uuid}", featureHandlers.DeleteFeaturePhase)

r.Post("/story", featureHandlers.CreateOrEditStory)
Expand Down