Skip to content

Commit

Permalink
Merge pull request #2462 from saithsab877/feature/bounty-timing-integ…
Browse files Browse the repository at this point in the history
…rations

feat: Implement Bounty Timing System Integration
  • Loading branch information
humansinstitute authored Jan 16, 2025
2 parents 7ac56dc + 0e6621b commit 9af789b
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 7 deletions.
23 changes: 17 additions & 6 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -1964,21 +1964,24 @@ func (db database) StartBountyTiming(bountyID uint) error {

timing, err = db.CreateBountyTiming(bountyID)
if err != nil {
return err
return fmt.Errorf("failed to create bounty timing: %w", err)
}
}

if timing.FirstAssignedAt == nil {
timing.FirstAssignedAt = &now
return db.UpdateBountyTiming(timing)
if err := db.UpdateBountyTiming(timing); err != nil {
return fmt.Errorf("failed to update bounty timing: %w", err)
}
}

return nil
}

func (db database) CloseBountyTiming(bountyID uint) error {
timing, err := db.GetBountyTiming(bountyID)
if err != nil {
return err
return fmt.Errorf("failed to retrieve bounty timing: %w", err)
}

now := time.Now()
Expand All @@ -1988,13 +1991,17 @@ func (db database) CloseBountyTiming(bountyID uint) error {
timing.TotalDurationSeconds = int(now.Sub(*timing.FirstAssignedAt).Seconds())
}

return db.UpdateBountyTiming(timing)
if err := db.UpdateBountyTiming(timing); err != nil {
return fmt.Errorf("failed to close bounty timing: %w", err)
}

return nil
}

func (db database) UpdateBountyTimingOnProof(bountyID uint) error {
timing, err := db.GetBountyTiming(bountyID)
if err != nil {
return err
return fmt.Errorf("failed to retrieve bounty timing: %w", err)
}

now := time.Now()
Expand All @@ -2007,7 +2014,11 @@ func (db database) UpdateBountyTimingOnProof(bountyID uint) error {
timing.LastPoWAt = &now
timing.TotalAttempts++

return db.UpdateBountyTiming(timing)
if err := db.UpdateBountyTiming(timing); err != nil {
return fmt.Errorf("failed to update bounty timing: %w", err)
}

return nil
}

func (db database) GetWorkspaceBountyCardsData(r *http.Request) []NewBounty {
Expand Down
30 changes: 29 additions & 1 deletion handlers/bounty.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ func NewBountyHandler(httpClient HttpClient, database db.Database) *bountyHandle
}
}

type TimingError struct {
Operation string `json:"operation"`
Error string `json:"error"`
}

func handleTimingError(w http.ResponseWriter, operation string, err error) {
logger.Log.Error("[bounty_timing] %s failed: %v", operation, err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(TimingError{
Operation: operation,
Error: err.Error(),
})
}

func (h *bountyHandler) GetAllBounties(w http.ResponseWriter, r *http.Request) {
bounties := h.db.GetAllBounties(r)
var bountyResponse []db.BountyResponse = h.GenerateBountyResponse(bounties)
Expand Down Expand Up @@ -262,6 +276,12 @@ func (h *bountyHandler) CreateOrEditBounty(w http.ResponseWriter, r *http.Reques
if bounty.Assignee != "" {
now := time.Now()
bounty.AssignedDate = &now

if bounty.ID != 0 {
if err := h.db.StartBountyTiming(bounty.ID); err != nil {
handleTimingError(w, "start_timing", err)
}
}
}

if bounty.Tribe == "" {
Expand Down Expand Up @@ -1685,7 +1705,11 @@ func (h *bountyHandler) AddProofOfWork(w http.ResponseWriter, r *http.Request) {
return
}

if err := h.db.IncrementProofCount(proof.BountyID); err != nil { // Pass the correct type (uint) here
if err := h.db.UpdateBountyTimingOnProof(proof.BountyID); err != nil {
handleTimingError(w, "update_timing_on_proof", err)
}

if err := h.db.IncrementProofCount(proof.BountyID); err != nil {
http.Error(w, "Failed to update bounty proof count", http.StatusInternalServerError)
return
}
Expand Down Expand Up @@ -1825,6 +1849,10 @@ func (h *bountyHandler) DeleteBountyAssignee(w http.ResponseWriter, r *http.Requ

h.db.UpdateBounty(b)

if err := h.db.CloseBountyTiming(b.ID); err != nil {
handleTimingError(w, "close_timing", err)
}

deletedAssignee = true
} else {
log.Printf("Could not delete bounty assignee")
Expand Down
213 changes: 213 additions & 0 deletions handlers/bounty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3728,3 +3728,216 @@ func TestBountyCardResponsePerformance(t *testing.T) {
assert.Equal(t, "Test Assignee", response[99].AssigneeName)
assert.Equal(t, "test-image-url", response[99].AssigneePic)
}

func TestBountyTiming(t *testing.T) {
teardownSuite := SetupSuite(t)
defer teardownSuite(t)

mockHttpClient := mocks.NewHttpClient(t)
mockDB := dbMocks.NewDatabase(t)
bHandler := NewBountyHandler(mockHttpClient, mockDB)

t.Run("GetBountyTimingStats", func(t *testing.T) {
t.Run("should return 400 for invalid bounty ID", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.GetBountyTimingStats)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "invalid")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodGet,
"/timing",
nil,
)
assert.NoError(t, err)

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})

t.Run("should return 500 when database fails", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.GetBountyTimingStats)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "1")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodGet,
"/timing",
nil,
)
assert.NoError(t, err)

mockDB.On("GetBountyTiming", uint(1)).Return(nil, fmt.Errorf("database error")).Once()

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
mockDB.AssertExpectations(t)
})

t.Run("should return timing stats successfully", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.GetBountyTimingStats)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "1")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodGet,
"/timing",
nil,
)
assert.NoError(t, err)

now := time.Now()
mockTiming := &db.BountyTiming{
BountyID: 1,
TotalWorkTimeSeconds: 3600,
TotalDurationSeconds: 7200,
TotalAttempts: 5,
FirstAssignedAt: &now,
LastPoWAt: &now,
ClosedAt: &now,
}

mockDB.On("GetBountyTiming", uint(1)).Return(mockTiming, nil).Once()

handler.ServeHTTP(rr, req)

assert.Equal(t, http.StatusOK, rr.Code)

var response BountyTimingResponse
err = json.NewDecoder(rr.Body).Decode(&response)
assert.NoError(t, err)
assert.Equal(t, mockTiming.TotalWorkTimeSeconds, response.TotalWorkTimeSeconds)
assert.Equal(t, mockTiming.TotalAttempts, response.TotalAttempts)
mockDB.AssertExpectations(t)
})
})

t.Run("StartBountyTiming", func(t *testing.T) {
t.Run("should return 400 for invalid bounty ID", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.StartBountyTiming)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "invalid")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodPut,
"/timing/start",
nil,
)
assert.NoError(t, err)

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})

t.Run("should return 500 when database fails", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.StartBountyTiming)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "1")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodPut,
"/timing/start",
nil,
)
assert.NoError(t, err)

mockDB.On("StartBountyTiming", uint(1)).Return(fmt.Errorf("database error")).Once()

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
mockDB.AssertExpectations(t)
})

t.Run("should start timing successfully", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.StartBountyTiming)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "1")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodPut,
"/timing/start",
nil,
)
assert.NoError(t, err)

mockDB.On("StartBountyTiming", uint(1)).Return(nil).Once()

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
mockDB.AssertExpectations(t)
})
})

t.Run("CloseBountyTiming", func(t *testing.T) {
t.Run("should return 400 for invalid bounty ID", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.CloseBountyTiming)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "invalid")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodPut,
"/timing/close",
nil,
)
assert.NoError(t, err)

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})

t.Run("should return 500 when database fails", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.CloseBountyTiming)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "1")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodPut,
"/timing/close",
nil,
)
assert.NoError(t, err)

mockDB.On("CloseBountyTiming", uint(1)).Return(fmt.Errorf("database error")).Once()

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
mockDB.AssertExpectations(t)
})

t.Run("should close timing successfully", func(t *testing.T) {
rr := httptest.NewRecorder()
handler := http.HandlerFunc(bHandler.CloseBountyTiming)

rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "1")
req, err := http.NewRequestWithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rctx),
http.MethodPut,
"/timing/close",
nil,
)
assert.NoError(t, err)

mockDB.On("CloseBountyTiming", uint(1)).Return(nil).Once()

handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
mockDB.AssertExpectations(t)
})
})
}

0 comments on commit 9af789b

Please sign in to comment.