diff --git a/db/db_test.go b/db/db_test.go new file mode 100644 index 000000000..85ebd6a14 --- /dev/null +++ b/db/db_test.go @@ -0,0 +1,138 @@ +package db + +import ( + "reflect" + "testing" +) + +func TestGetFilterStatusCount(t *testing.T) { + + InitTestDB() + + tests := []struct { + name string + setup []NewBounty + expected FilterStattuCount + }{ + { + name: "Empty Database", + setup: []NewBounty{}, + expected: FilterStattuCount{ + Open: 0, Assigned: 0, Completed: 0, + Paid: 0, Pending: 0, Failed: 0, + }, + }, + { + name: "Hidden Bounties Should Not Count", + setup: []NewBounty{ + {Show: false, Assignee: "", Paid: false}, + {Show: false, Assignee: "user1", Completed: true}, + }, + expected: FilterStattuCount{ + Open: 0, Assigned: 0, Completed: 0, + Paid: 0, Pending: 0, Failed: 0, + }, + }, + { + name: "Open Bounties Count", + setup: []NewBounty{ + {Show: true, Assignee: "", Paid: false}, + {Show: true, Assignee: "", Paid: false}, + }, + expected: FilterStattuCount{ + Open: 2, Assigned: 0, Completed: 0, + Paid: 0, Pending: 0, Failed: 0, + }, + }, + { + name: "Assigned Bounties Count", + setup: []NewBounty{ + {Show: true, Assignee: "user1", Paid: false}, + {Show: true, Assignee: "user2", Paid: false}, + }, + expected: FilterStattuCount{ + Open: 0, Assigned: 2, Completed: 0, + Paid: 0, Pending: 0, Failed: 0, + }, + }, + { + name: "Completed Bounties Count", + setup: []NewBounty{ + {Show: true, Assignee: "user1", Completed: true, Paid: false}, + {Show: true, Assignee: "user2", Completed: true, Paid: false}, + }, + expected: FilterStattuCount{ + Open: 0, Assigned: 2, Completed: 2, + Paid: 0, Pending: 0, Failed: 0, + }, + }, + { + name: "Paid Bounties Count", + setup: []NewBounty{ + {Show: true, Assignee: "user1", Paid: true}, + {Show: true, Assignee: "user2", Paid: true}, + }, + expected: FilterStattuCount{ + Open: 0, Assigned: 0, Completed: 0, + Paid: 2, Pending: 0, Failed: 0, + }, + }, + { + name: "Pending Payment Bounties Count", + setup: []NewBounty{ + {Show: true, Assignee: "user1", PaymentPending: true}, + {Show: true, Assignee: "user2", PaymentPending: true}, + }, + expected: FilterStattuCount{ + Open: 0, Assigned: 2, Completed: 0, + Paid: 0, Pending: 2, Failed: 0, + }, + }, + { + name: "Failed Payment Bounties Count", + setup: []NewBounty{ + {Show: true, Assignee: "user1", PaymentFailed: true}, + {Show: true, Assignee: "user2", PaymentFailed: true}, + }, + expected: FilterStattuCount{ + Open: 0, Assigned: 2, Completed: 0, + Paid: 0, Pending: 0, Failed: 2, + }, + }, + { + name: "Mixed Status Bounties", + setup: []NewBounty{ + {Show: true, Assignee: "", Paid: false}, + {Show: true, Assignee: "user1", Paid: false}, + {Show: true, Assignee: "user2", Completed: true, Paid: false}, + {Show: true, Assignee: "user3", Paid: true}, + {Show: true, Assignee: "user4", PaymentPending: true}, + {Show: true, Assignee: "user5", PaymentFailed: true}, + {Show: false, Assignee: "user6", Paid: true}, + }, + expected: FilterStattuCount{ + Open: 1, Assigned: 4, Completed: 1, + Paid: 1, Pending: 1, Failed: 1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + TestDB.DeleteAllBounties() + + for _, bounty := range tt.setup { + if err := TestDB.db.Create(&bounty).Error; err != nil { + t.Fatalf("Failed to create test bounty: %v", err) + } + } + + result := TestDB.GetFilterStatusCount() + + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("GetFilterStatusCount() = %+v, want %+v", result, tt.expected) + } + }) + } +} diff --git a/db/test_config.go b/db/test_config.go index 48fda26ce..d9a49e52d 100644 --- a/db/test_config.go +++ b/db/test_config.go @@ -94,3 +94,7 @@ func CleanTestData() { TestDB.db.Exec("DELETE FROM people") } + +func DeleteAllChatMessages() { + TestDB.db.Exec("DELETE FROM chat_messages") +} diff --git a/handlers/auth_test.go b/handlers/auth_test.go index c917135f5..400cb2c32 100644 --- a/handlers/auth_test.go +++ b/handlers/auth_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "os" @@ -46,12 +47,160 @@ func TestGetAdminPubkeys(t *testing.T) { expected := `{"pubkeys":["test"]}` if strings.TrimRight(rr.Body.String(), "\n") != expected { - t.Errorf("handler returned unexpected body: expected %s pubkeys %s is there a space after?", expected, rr.Body.String()) } }) -} + t.Run("Should handle multiple admin pubkeys", func(t *testing.T) { + os.Setenv("ADMINS", "test1,test2,test3") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":["test1","test2","test3"]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) + + t.Run("Should handle empty admin pubkeys", func(t *testing.T) { + os.Setenv("ADMINS", "") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":[]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) + + t.Run("Should handle admin pubkeys with special characters", func(t *testing.T) { + os.Setenv("ADMINS", "test@123,test#456,test$789") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":["test@123","test#456","test$789"]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) + + t.Run("Should handle admin pubkeys with spaces", func(t *testing.T) { + os.Setenv("ADMINS", "test 123, test 456 , test 789") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":["test 123","test 456","test 789"]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) + + t.Run("Should handle invalid HTTP method", func(t *testing.T) { + os.Setenv("ADMINS", "test") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest(http.MethodPost, "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":["test"]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) + t.Run("Maximum Number of Admin Keys", func(t *testing.T) { + + var keys []string + for i := 0; i < 1000; i++ { + keys = append(keys, fmt.Sprintf("key%d", i)) + } + os.Setenv("ADMINS", strings.Join(keys, ",")) + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + var response map[string][]string + err = json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, 1000, len(response["pubkeys"])) + }) + + t.Run("Null Admin Keys List", func(t *testing.T) { + os.Unsetenv("ADMINS") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":[]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) + + t.Run("Unicode Characters in Admin Keys", func(t *testing.T) { + os.Setenv("ADMINS", "ключ1,キー2,钥匙3") + os.Setenv("RELAY_URL", "RelayUrl") + os.Setenv("RELAY_AUTH_KEY", "RelayAuthKey") + config.InitConfig() + + req, err := http.NewRequest("GET", "/admin_pubkeys", nil) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + handler := http.HandlerFunc(GetAdminPubkeys) + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + expected := `{"pubkeys":["ключ1","キー2","钥匙3"]}` + assert.JSONEq(t, expected, strings.TrimRight(rr.Body.String(), "\n")) + }) +} func TestCreateConnectionCode(t *testing.T) { teardownSuite := SetupSuite(t) defer teardownSuite(t) @@ -287,6 +436,115 @@ func TestGetIsAdmin(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) }) + + t.Run("Should test that empty public key returns unauthorized", func(t *testing.T) { + req, err := http.NewRequest("GET", "/admin/auth", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(aHandler.GetIsAdmin) + + ctx := context.WithValue(req.Context(), auth.ContextKey, "") + req = req.WithContext(ctx) + + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + var responseBody string + json.NewDecoder(rr.Body).Decode(&responseBody) + assert.Equal(t, "Not a super admin: handler", responseBody) + }) + + t.Run("Should test that nil context value returns unauthorized", func(t *testing.T) { + req, err := http.NewRequest("GET", "/admin/auth", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(aHandler.GetIsAdmin) + + ctx := context.WithValue(req.Context(), auth.ContextKey, nil) + req = req.WithContext(ctx) + + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + var responseBody string + json.NewDecoder(rr.Body).Decode(&responseBody) + assert.Equal(t, "Not a super admin: handler", responseBody) + }) + + t.Run("Should test that free pass enabled allows any user", func(t *testing.T) { + + originalAdmins := config.SuperAdmins + config.SuperAdmins = []string{config.AdminDevFreePass} + defer func() { + config.SuperAdmins = originalAdmins + }() + + req, err := http.NewRequest("GET", "/admin/auth", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(aHandler.GetIsAdmin) + + ctx := context.WithValue(req.Context(), auth.ContextKey, "any_pubkey") + req = req.WithContext(ctx) + + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + var responseBody string + json.NewDecoder(rr.Body).Decode(&responseBody) + assert.Equal(t, "Log in successful", responseBody) + }) + + t.Run("Should test that invalid context value type returns unauthorized", func(t *testing.T) { + req, err := http.NewRequest("GET", "/admin/auth", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(aHandler.GetIsAdmin) + + ctx := context.WithValue(req.Context(), auth.ContextKey, 12345) + req = req.WithContext(ctx) + + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + var responseBody string + json.NewDecoder(rr.Body).Decode(&responseBody) + assert.Equal(t, "Not a super admin: handler", responseBody) + }) + + t.Run("Should test multiple admins configuration", func(t *testing.T) { + + originalAdmins := config.SuperAdmins + config.SuperAdmins = []string{"admin1", "admin2", "admin3"} + defer func() { + config.SuperAdmins = originalAdmins + }() + + req, err := http.NewRequest("GET", "/admin/auth", nil) + if err != nil { + t.Fatal(err) + } + rr := httptest.NewRecorder() + handler := http.HandlerFunc(aHandler.GetIsAdmin) + + ctx := context.WithValue(req.Context(), auth.ContextKey, "admin2") + req = req.WithContext(ctx) + + handler.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + var responseBody string + json.NewDecoder(rr.Body).Decode(&responseBody) + assert.Equal(t, "Log in successful", responseBody) + }) } func TestRefreshToken(t *testing.T) { diff --git a/handlers/bounties.go b/handlers/bounties.go index d1e245f02..af7287694 100644 --- a/handlers/bounties.go +++ b/handlers/bounties.go @@ -2,15 +2,11 @@ package handlers import ( "encoding/json" - "io" - "log" - "net/http" - "reflect" - "strconv" - "github.com/lib/pq" "github.com/stakwork/sphinx-tribes/db" "github.com/stakwork/sphinx-tribes/logger" + "net/http" + "reflect" ) func GetWantedsHeader(w http.ResponseWriter, r *http.Request) { @@ -44,49 +40,6 @@ func GetBountiesLeaderboard(w http.ResponseWriter, _ *http.Request) { json.NewEncoder(w).Encode(leaderBoard) } -func DeleteBountyAssignee(w http.ResponseWriter, r *http.Request) { - invoice := db.DeleteBountyAssignee{} - body, err := io.ReadAll(r.Body) - var deletedAssignee bool - - r.Body.Close() - - err = json.Unmarshal(body, &invoice) - - if err != nil { - logger.Log.Error("%v", err) - w.WriteHeader(http.StatusNotAcceptable) - return - } - - owner_key := invoice.Owner_pubkey - date := invoice.Created - - createdUint, _ := strconv.ParseUint(date, 10, 32) - b, err := db.DB.GetBountyByCreated(uint(createdUint)) - - if err == nil && b.OwnerID == owner_key { - b.Assignee = "" - b.AssignedHours = 0 - b.CommitmentFee = 0 - b.BountyExpires = "" - - db.DB.UpdateBounty(b) - - deletedAssignee = true - } else { - log.Printf("Could not delete bounty assignee") - - deletedAssignee = false - - w.WriteHeader(http.StatusBadRequest) - json.NewEncoder(w).Encode(deletedAssignee) - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(deletedAssignee) -} - func MigrateBounties(w http.ResponseWriter, r *http.Request) { peeps := db.DB.GetAllPeople() diff --git a/handlers/bounty.go b/handlers/bounty.go index f61a1b2ee..244985863 100644 --- a/handlers/bounty.go +++ b/handlers/bounty.go @@ -1643,6 +1643,7 @@ func isValidProofStatus(status db.ProofOfWorkStatus) bool { } return false } + func (h *bountyHandler) DeleteBountyAssignee(w http.ResponseWriter, r *http.Request) { invoice := db.DeleteBountyAssignee{} body, err := io.ReadAll(r.Body) @@ -1690,4 +1691,4 @@ func (h *bountyHandler) DeleteBountyAssignee(w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(deletedAssignee) -} +} \ No newline at end of file diff --git a/handlers/bounty_test.go b/handlers/bounty_test.go index 8f1fdd319..1977fe7d2 100644 --- a/handlers/bounty_test.go +++ b/handlers/bounty_test.go @@ -2392,3 +2392,172 @@ func TestGetBountyCards(t *testing.T) { assert.Empty(t, cardWithoutAssignee.AssigneePic) }) } + +func TestDeleteBountyAssignee(t *testing.T) { + + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + mockHttpClient := mocks.NewHttpClient(t) + + bHandler := NewBountyHandler(mockHttpClient, db.TestDB) + + db.CleanTestData() + + db.TestDB.CreateOrEditBounty(db.NewBounty{ + Type: "coding", + Title: "Bounty 1", + Description: "Description for Bounty 1", + WorkspaceUuid: "work-1", + OwnerID: "validOwner", + Price: 1500, + Created: 1234567890, + }) + + db.TestDB.CreateOrEditBounty(db.NewBounty{ + Type: "design", + Title: "Bounty 2", + Description: "Description for Bounty 2", + WorkspaceUuid: "work-2", + OwnerID: "nonExistentOwner", + Price: 2000, + Created: 1234567891, + }) + + db.TestDB.CreateOrEditBounty(db.NewBounty{ + Type: "design", + Title: "Bounty 2", + Description: "Description for Bounty 2", + WorkspaceUuid: "work-2", + OwnerID: "validOwner", + Price: 2000, + Created: 0, + }) + + tests := []struct { + name string + input interface{} + mockSetup func() + expectedStatus int + expectedBody bool + }{ + { + name: "Valid Input - Successful Deletion", + input: db.DeleteBountyAssignee{ + Owner_pubkey: "validOwner", + Created: "1234567890", + }, + expectedStatus: http.StatusOK, + expectedBody: true, + }, + { + name: "Empty JSON Body", + input: nil, + expectedStatus: http.StatusNotAcceptable, + expectedBody: false, + }, + { + name: "Invalid JSON Format", + input: `{"Owner_pubkey": "abc", "Created": }`, + expectedStatus: http.StatusNotAcceptable, + expectedBody: false, + }, + { + name: "Non-Existent Bounty", + input: db.DeleteBountyAssignee{ + Owner_pubkey: "nonExistentOwner", + Created: "1234567890", + }, + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Mismatched Owner Key", + input: db.DeleteBountyAssignee{ + Owner_pubkey: "wrongOwner", + Created: "1234567890", + }, + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Invalid Data Types", + input: db.DeleteBountyAssignee{ + Owner_pubkey: "validOwners", + Created: "invalidDate", + }, + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Null Values", + input: db.DeleteBountyAssignee{ + Owner_pubkey: "", + Created: "", + }, + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Large JSON Body", + input: map[string]interface{}{ + "Owner_pubkey": "validOwner", + "Created": "1234567890", + "Extra": make([]byte, 10000), + }, + expectedStatus: http.StatusOK, + expectedBody: true, + }, + { + name: "Boundary Date Value", + input: db.DeleteBountyAssignee{ + Owner_pubkey: "validOwner", + Created: "0", + }, + expectedStatus: http.StatusOK, + expectedBody: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + var body []byte + if tt.input != nil { + switch v := tt.input.(type) { + case string: + body = []byte(v) + default: + var err error + body, err = json.Marshal(tt.input) + if err != nil { + t.Fatalf("Failed to marshal input: %v", err) + } + } + } + + req := httptest.NewRequest(http.MethodDelete, "/gobounties/assignee", bytes.NewReader(body)) + + w := httptest.NewRecorder() + + bHandler.DeleteBountyAssignee(w, req) + + resp := w.Result() + defer resp.Body.Close() + + assert.Equal(t, tt.expectedStatus, resp.StatusCode) + + if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest { + + var result bool + err := json.NewDecoder(resp.Body).Decode(&result) + if err != nil { + t.Fatalf("Failed to decode response body: %v", err) + } + + assert.Equal(t, tt.expectedBody, result) + } + }) + } + +} diff --git a/handlers/chat_test.go b/handlers/chat_test.go index 447b3f18d..126ed0cd0 100644 --- a/handlers/chat_test.go +++ b/handlers/chat_test.go @@ -827,3 +827,283 @@ func TestProcessChatResponse(t *testing.T) { }) } } + + +func TestGetChatHistory(t *testing.T) { + teardownSuite := SetupSuite(t) + defer teardownSuite(t) + + chatHandler := NewChatHandler(&http.Client{}, db.TestDB) + + t.Run("should successfully get chat history when valid chat_id is provided", func(t *testing.T) { + + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: "workspace1", + Title: "Test Chat", + } + db.TestDB.AddChat(chat) + + messages := []db.ChatMessage{ + { + ID: uuid.New().String(), + ChatID: chat.ID, + Message: "Message 1", + Role: "user", + Timestamp: time.Now(), + }, + { + ID: uuid.New().String(), + ChatID: chat.ID, + Message: "Message 2", + Role: "assistant", + Timestamp: time.Now(), + }, + } + for _, msg := range messages { + db.TestDB.AddChatMessage(&msg) + } + + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", chat.ID) + req, err := http.NewRequestWithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodGet, + "/hivechat/history/"+chat.ID, + nil, + ) + assert.NoError(t, err) + + handler := http.HandlerFunc(chatHandler.GetChatHistory) + handler.ServeHTTP(rr, req) + + var response HistoryChatResponse + err = json.NewDecoder(rr.Body).Decode(&response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + responseMessages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Equal(t, 2, len(responseMessages)) + }) + + t.Run("should return bad request when chat_id is missing", func(t *testing.T) { + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + req, err := http.NewRequestWithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodGet, + "/hivechat/history/", + nil, + ) + assert.NoError(t, err) + + handler := http.HandlerFunc(chatHandler.GetChatHistory) + handler.ServeHTTP(rr, req) + + var response ChatResponse + err = json.NewDecoder(rr.Body).Decode(&response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.False(t, response.Success) + assert.Equal(t, "Chat ID is required", response.Message) + }) + + t.Run("should return empty array when chat has no messages", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: "workspace1", + Title: "Empty Chat", + } + db.TestDB.AddChat(chat) + + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", chat.ID) + req, err := http.NewRequestWithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodGet, + "/hivechat/history/"+chat.ID, + nil, + ) + assert.NoError(t, err) + + handler := http.HandlerFunc(chatHandler.GetChatHistory) + handler.ServeHTTP(rr, req) + + var response HistoryChatResponse + err = json.NewDecoder(rr.Body).Decode(&response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + responseMessages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Empty(t, responseMessages) + }) + + t.Run("should handle chat with large number of messages", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: "workspace1", + Title: "Large Chat", + } + db.TestDB.AddChat(chat) + + for i := 0; i < 100; i++ { + message := &db.ChatMessage{ + ID: uuid.New().String(), + ChatID: chat.ID, + Message: fmt.Sprintf("Message %d", i), + Role: "user", + Timestamp: time.Now(), + } + db.TestDB.AddChatMessage(message) + } + + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", chat.ID) + req, err := http.NewRequestWithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rctx), + http.MethodGet, + "/hivechat/history/"+chat.ID, + nil, + ) + assert.NoError(t, err) + + handler := http.HandlerFunc(chatHandler.GetChatHistory) + handler.ServeHTTP(rr, req) + + var response HistoryChatResponse + err = json.NewDecoder(rr.Body).Decode(&response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + responseMessages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Equal(t, 100, len(responseMessages)) + }) + + t.Run("Valid Chat ID with No Messages", func(t *testing.T) { + chat := &db.Chat{ + ID: uuid.New().String(), + WorkspaceID: "workspace1", + Title: "Empty Chat", + } + db.TestDB.AddChat(chat) + + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", chat.ID) + req := httptest.NewRequest(http.MethodGet, "/hivechat/history/"+chat.ID, nil) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.GetChatHistory(rr, req) + + var response HistoryChatResponse + json.NewDecoder(rr.Body).Decode(&response) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + messages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Empty(t, messages) + }) + + t.Run("Chat ID with Special Characters", func(t *testing.T) { + + db.DeleteAllChats() + db.DeleteAllChatMessages() + + specialChatID := "special!@#$%^&*()_+-=[]{}|;:,.<>?" + chat := &db.Chat{ + ID: specialChatID, + WorkspaceID: "workspace1", + Title: "Special Chat", + } + db.TestDB.AddChat(chat) + + message := &db.ChatMessage{ + ID: uuid.New().String(), + ChatID: specialChatID, + Message: "Special message", + Role: "user", + Timestamp: time.Now(), + } + db.TestDB.AddChatMessage(message) + + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", specialChatID) + req := httptest.NewRequest( + http.MethodGet, + "/hivechat/history/"+url.PathEscape(specialChatID), + nil, + ) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.GetChatHistory(rr, req) + + var response HistoryChatResponse + err := json.NewDecoder(rr.Body).Decode(&response) + assert.NoError(t, err) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + + messages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Equal(t, 1, len(messages)) + + firstMessage := messages[0].(map[string]interface{}) + assert.Equal(t, specialChatID, firstMessage["chatId"]) + assert.Equal(t, "Special message", firstMessage["message"]) + assert.Equal(t, "user", firstMessage["role"]) + }) + + t.Run("Non-Existent Chat ID", func(t *testing.T) { + nonExistentID := uuid.New().String() + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", nonExistentID) + req := httptest.NewRequest(http.MethodGet, "/hivechat/history/"+nonExistentID, nil) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.GetChatHistory(rr, req) + + var response HistoryChatResponse + json.NewDecoder(rr.Body).Decode(&response) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + messages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Empty(t, messages) + }) + + t.Run("Malformed Chat ID", func(t *testing.T) { + malformedID := "malformed-chat-id-###" + rr := httptest.NewRecorder() + rctx := chi.NewRouteContext() + rctx.URLParams.Add("uuid", malformedID) + req := httptest.NewRequest(http.MethodGet, "/hivechat/history/"+malformedID, nil) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + chatHandler.GetChatHistory(rr, req) + + var response HistoryChatResponse + json.NewDecoder(rr.Body).Decode(&response) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.True(t, response.Success) + messages, ok := response.Data.([]interface{}) + assert.True(t, ok) + assert.Empty(t, messages) + }) + +} diff --git a/routes/bounty.go b/routes/bounty.go index ba69dcd95..73efa1b3c 100644 --- a/routes/bounty.go +++ b/routes/bounty.go @@ -47,7 +47,7 @@ func BountyRoutes() chi.Router { r.Patch("/{id}/proofs/{proofId}/status", bountyHandler.UpdateProofStatus) r.Post("/", bountyHandler.CreateOrEditBounty) - r.Delete("/assignee", handlers.DeleteBountyAssignee) + r.Delete("/assignee", bountyHandler.DeleteBountyAssignee) r.Delete("/{pubkey}/{created}", bountyHandler.DeleteBounty) r.Post("/paymentstatus/{created}", handlers.UpdatePaymentStatus) r.Post("/completedstatus/{created}", handlers.UpdateCompletedStatus) diff --git a/utils/ticket_processor.go b/utils/ticket_processor.go index 2535fac93..a5cab5b3b 100644 --- a/utils/ticket_processor.go +++ b/utils/ticket_processor.go @@ -2,6 +2,7 @@ package utils import ( "errors" + "strings" ) type TicketReviewRequest struct { @@ -16,10 +17,13 @@ type TicketReviewRequest struct { } func ValidateTicketReviewRequest(req *TicketReviewRequest) error { - if req.Value.TicketUUID == "" { + if req == nil { + return errors.New("nil request") + } + if strings.TrimSpace(req.Value.TicketUUID) == "" { return errors.New("ticketUUID is required") } - if req.Value.TicketDescription == "" { + if strings.TrimSpace(req.Value.TicketDescription) == "" { return errors.New("ticketDescription is required") } return nil diff --git a/utils/ticket_processor_test.go b/utils/ticket_processor_test.go new file mode 100644 index 000000000..f6a022488 --- /dev/null +++ b/utils/ticket_processor_test.go @@ -0,0 +1,185 @@ +package utils + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateTicketReviewRequest(t *testing.T) { + tests := []struct { + name string + input *TicketReviewRequest + wantErr string + }{ + { + name: "valid request", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "test-uuid", + TicketDescription: "test description", + }, + }, + wantErr: "", + }, + { + name: "missing ticket UUID", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketDescription: "test description", + }, + }, + wantErr: "ticketUUID is required", + }, + { + name: "missing ticket description", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "test-uuid", + }, + }, + wantErr: "ticketDescription is required", + }, + { + name: "Both TicketUUID and TicketDescription Empty", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "", + TicketDescription: "", + }, + }, + wantErr: "ticketUUID is required", + }, + { + name: "Nil Request", + input: nil, + wantErr: "nil request", + }, + { + name: "Whitespace TicketUUID", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: " ", + TicketDescription: "This is a valid ticket description", + }, + }, + wantErr: "ticketUUID is required", + }, + { + name: "Whitespace TicketDescription", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "123e4567-e89b-12d3-a456-426614174000", + TicketDescription: " ", + }, + }, + wantErr: "ticketDescription is required", + }, + { + name: "Large TicketDescription", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "123e4567-e89b-12d3-a456-426614174000", + TicketDescription: strings.Repeat("a", 10000), + }, + }, + wantErr: "", + }, + { + name: "Non-UUID TicketUUID", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "not-a-uuid", + TicketDescription: "This is a valid ticket description", + }, + }, + wantErr: "", + }, + { + name: "With Optional Fields", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + FeatureUUID: "feature-123", + PhaseUUID: "phase-456", + TicketUUID: "ticket-789", + TicketDescription: "test description", + }, + }, + wantErr: "", + }, + { + name: "With RequestUUID and SourceWebsocket", + input: &TicketReviewRequest{ + Value: struct { + FeatureUUID string `json:"featureUUID"` + PhaseUUID string `json:"phaseUUID"` + TicketUUID string `json:"ticketUUID" validate:"required"` + TicketDescription string `json:"ticketDescription" validate:"required"` + }{ + TicketUUID: "ticket-789", + TicketDescription: "test description", + }, + RequestUUID: "req-123", + SourceWebsocket: "ws://example.com", + }, + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateTicketReviewRequest(tt.input) + if tt.wantErr == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, tt.wantErr) + } + }) + } +} \ No newline at end of file