package gitea_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "gitea.weiker.me/rodin/review-bot/gitea" "gitea.weiker.me/rodin/review-bot/vcs" ) func TestAdapter_GetPullRequest(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "title": "Test PR", "body": "PR body", "head": map[string]any{ "sha": "abc123", "ref": "feature-branch", }, "base": map[string]any{ "ref": "main", }, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) pr, err := adapter.GetPullRequest(context.Background(), "owner", "repo", 42) if err != nil { t.Fatalf("unexpected error: %v", err) } if pr.Number != 42 { t.Errorf("Number = %d, want 42", pr.Number) } if pr.Title != "Test PR" { t.Errorf("Title = %q, want %q", pr.Title, "Test PR") } if pr.Body != "PR body" { t.Errorf("Body = %q, want %q", pr.Body, "PR body") } if pr.Head.SHA != "abc123" { t.Errorf("Head.SHA = %q, want %q", pr.Head.SHA, "abc123") } if pr.Head.Ref != "feature-branch" { t.Errorf("Head.Ref = %q, want %q", pr.Head.Ref, "feature-branch") } if pr.Base.Ref != "main" { t.Errorf("Base.Ref = %q, want %q", pr.Base.Ref, "main") } } func TestAdapter_GetPullRequestFiles(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]any{ {"filename": "main.go", "status": "modified"}, {"filename": "new.go", "status": "added"}, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) files, err := adapter.GetPullRequestFiles(context.Background(), "owner", "repo", 1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(files) != 2 { t.Fatalf("got %d files, want 2", len(files)) } if files[0].Filename != "main.go" || files[0].Status != "modified" { t.Errorf("files[0] = %+v", files[0]) } if files[1].Filename != "new.go" || files[1].Status != "added" { t.Errorf("files[1] = %+v", files[1]) } } func TestAdapter_ListReviews(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]any{ { "id": 1, "body": "LGTM", "user": map[string]any{"login": "reviewer1"}, "state": "APPROVED", "stale": false, "commit_id": "abc123", }, { "id": 2, "body": "Needs work", "user": map[string]any{"login": "reviewer2"}, "state": "REQUEST_CHANGES", "stale": true, "commit_id": "def456", }, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) reviews, err := adapter.ListReviews(context.Background(), "owner", "repo", 1) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(reviews) != 2 { t.Fatalf("got %d reviews, want 2", len(reviews)) } if reviews[0].ID != 1 || reviews[0].Body != "LGTM" || reviews[0].User.Login != "reviewer1" { t.Errorf("reviews[0] = %+v", reviews[0]) } if reviews[0].State != "APPROVED" || reviews[0].Stale || reviews[0].CommitID != "abc123" { t.Errorf("reviews[0] state/stale/commit = %v/%v/%v", reviews[0].State, reviews[0].Stale, reviews[0].CommitID) } if reviews[1].ID != 2 || !reviews[1].Stale || reviews[1].State != "REQUEST_CHANGES" { t.Errorf("reviews[1] = %+v", reviews[1]) } } func TestAdapter_GetCommitStatuses(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]any{ { "status": "success", "context": "ci/test", "description": "All tests pass", "target_url": "https://ci.example.com/1", }, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) statuses, err := adapter.GetCommitStatuses(context.Background(), "owner", "repo", "abc123") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(statuses) != 1 { t.Fatalf("got %d statuses, want 1", len(statuses)) } if statuses[0].Status != "success" { t.Errorf("Status = %q, want %q", statuses[0].Status, "success") } if statuses[0].Context != "ci/test" { t.Errorf("Context = %q, want %q", statuses[0].Context, "ci/test") } if statuses[0].Description != "All tests pass" { t.Errorf("Description = %q, want %q", statuses[0].Description, "All tests pass") } if statuses[0].TargetURL != "https://ci.example.com/1" { t.Errorf("TargetURL = %q, want %q", statuses[0].TargetURL, "https://ci.example.com/1") } } func TestAdapter_PostReview_EventTranslation(t *testing.T) { tests := []struct { name string event vcs.ReviewEvent wantEvent string }{ {"APPROVE becomes APPROVED", vcs.ReviewEventApprove, "APPROVED"}, {"REQUEST_CHANGES stays", vcs.ReviewEventRequestChanges, "REQUEST_CHANGES"}, {"COMMENT stays", vcs.ReviewEventComment, "COMMENT"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var gotEvent string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var payload struct { Event string `json:"event"` } json.NewDecoder(r.Body).Decode(&payload) gotEvent = payload.Event json.NewEncoder(w).Encode(map[string]any{ "id": 1, "body": "test", "user": map[string]any{"login": "bot"}, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) _, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ Body: "test", Event: tt.event, // No comments → no diff fetch needed }) if err != nil { t.Fatalf("unexpected error: %v", err) } if gotEvent != tt.wantEvent { t.Errorf("event = %q, want %q", gotEvent, tt.wantEvent) } }) } } func TestAdapter_PostReview_CommitID(t *testing.T) { var gotCommitID string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var payload struct { CommitID string `json:"commit_id"` } json.NewDecoder(r.Body).Decode(&payload) gotCommitID = payload.CommitID json.NewEncoder(w).Encode(map[string]any{ "id": 1, "body": "test", "user": map[string]any{"login": "bot"}, "commit_id": payload.CommitID, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) _, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ Body: "test", Event: vcs.ReviewEventApprove, CommitID: "sha256abc", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if gotCommitID != "sha256abc" { t.Errorf("expected commit_id %q forwarded to client, got %q", "sha256abc", gotCommitID) } } func TestAdapter_PostReview_WithComments_PositionTranslation(t *testing.T) { diff := `diff --git a/main.go b/main.go --- a/main.go +++ b/main.go @@ -1,3 +1,4 @@ package main +// new comment at line 3 func main() {} ` var gotComments []struct { Path string `json:"path"` NewPosition int64 `json:"new_position"` Body string `json:"body"` } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if strings.HasSuffix(r.URL.Path, ".diff") { // Diff request w.Write([]byte(diff)) return } if strings.HasSuffix(r.URL.Path, "/reviews") { // Review post var payload struct { Comments []struct { Path string `json:"path"` NewPosition int64 `json:"new_position"` Body string `json:"body"` } `json:"comments"` } json.NewDecoder(r.Body).Decode(&payload) gotComments = payload.Comments json.NewEncoder(w).Encode(map[string]any{ "id": 1, "body": "review", "user": map[string]any{"login": "bot"}, }) return } t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) w.WriteHeader(http.StatusNotFound) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) // Position 4 in this diff is "+// new comment at line 3" → new line 3 _, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ Body: "review", Event: vcs.ReviewEventRequestChanges, Comments: []vcs.ReviewComment{ { Path: "main.go", Position: 4, CommitID: "abc123", Body: "needs fix", }, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(gotComments) != 1 { t.Fatalf("got %d comments, want 1", len(gotComments)) } if gotComments[0].Path != "main.go" { t.Errorf("path = %q, want %q", gotComments[0].Path, "main.go") } if gotComments[0].NewPosition != 3 { t.Errorf("new_position = %d, want 3", gotComments[0].NewPosition) } if gotComments[0].Body != "needs fix" { t.Errorf("body = %q, want %q", gotComments[0].Body, "needs fix") } } func TestAdapter_DismissReview(t *testing.T) { var deleteCalled bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodDelete { deleteCalled = true w.WriteHeader(204) return } w.WriteHeader(404) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) err := adapter.DismissReview(context.Background(), "owner", "repo", 1, 99, "stale review") if err != nil { t.Fatalf("unexpected error: %v", err) } if !deleteCalled { t.Error("expected delete to be called") } } func TestAdapter_Underlying(t *testing.T) { client := gitea.NewClient("http://example.com", "token") adapter := gitea.NewAdapter(client) if adapter.Underlying() != client { t.Error("Underlying() should return the wrapped client") } } func TestAdapter_ListContents(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode([]map[string]any{ {"name": "main.go", "path": "src/main.go", "type": "file"}, {"name": "util", "path": "src/util", "type": "dir"}, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) entries, err := adapter.ListContents(context.Background(), "owner", "repo", "src") if err != nil { t.Fatalf("unexpected error: %v", err) } if len(entries) != 2 { t.Fatalf("got %d entries, want 2", len(entries)) } if entries[0].Name != "main.go" || entries[0].Type != "file" { t.Errorf("entries[0] = %+v", entries[0]) } if entries[1].Name != "util" || entries[1].Type != "dir" { t.Errorf("entries[1] = %+v", entries[1]) } } func TestAdapter_GetFileContent_RefRouting(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // When ref is provided, the URL should contain ?ref= if r.URL.RawQuery != "" && strings.Contains(r.URL.RawQuery, "ref=") { w.Write([]byte("content-at-ref")) } else { w.Write([]byte("content-default")) } })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) // Empty ref → routes to GetFileContent (no ?ref= query param) got, err := adapter.GetFileContent(context.Background(), "owner", "repo", "main.go", "") if err != nil { t.Fatalf("GetFileContent(ref=\"\"): %v", err) } if got != "content-default" { t.Errorf("GetFileContent(ref=\"\") = %q, want %q", got, "content-default") } // Non-empty ref → routes to GetFileContentRef (with ?ref= query param) got, err = adapter.GetFileContent(context.Background(), "owner", "repo", "main.go", "abc123") if err != nil { t.Fatalf("GetFileContent(ref=\"abc123\"): %v", err) } if got != "content-at-ref" { t.Errorf("GetFileContent(ref=\"abc123\") = %q, want %q", got, "content-at-ref") } } func TestAdapter_RequestReviewerSelf(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("expected POST, got %s", r.Method) } expected := "/api/v1/repos/owner/repo/pulls/5/requested_reviewers" if r.URL.Path != expected { t.Errorf("path = %q, want %q", r.URL.Path, expected) } w.WriteHeader(http.StatusCreated) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) err := adapter.RequestReviewerSelf(context.Background(), "owner", "repo", 5, "bot-user") if err != nil { t.Fatalf("RequestReviewerSelf() error = %v", err) } } func TestAdapter_PostReview_CommitID_Threading(t *testing.T) { var gotPayload struct { Body string `json:"body"` Event string `json:"event"` CommitID string `json:"commit_id"` } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewDecoder(r.Body).Decode(&gotPayload) json.NewEncoder(w).Encode(map[string]any{ "id": 1, "body": "test", "user": map[string]any{"login": "bot"}, "commit_id": "abc123def456", }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) review, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ Body: "LGTM", Event: vcs.ReviewEventApprove, CommitID: "abc123def456", // No comments → no diff fetch needed }) if err != nil { t.Fatalf("unexpected error: %v", err) } if gotPayload.CommitID != "abc123def456" { t.Errorf("commit_id = %q, want %q", gotPayload.CommitID, "abc123def456") } if review.CommitID != "abc123def456" { t.Errorf("review.CommitID = %q, want %q", review.CommitID, "abc123def456") } } func TestAdapter_PostReview_EmptyCommitID_Omitted(t *testing.T) { var gotRawPayload map[string]any server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewDecoder(r.Body).Decode(&gotRawPayload) json.NewEncoder(w).Encode(map[string]any{ "id": 1, "body": "test", "user": map[string]any{"login": "bot"}, }) })) defer server.Close() client := gitea.NewClient(server.URL, "token") adapter := gitea.NewAdapter(client) _, err := adapter.PostReview(context.Background(), "owner", "repo", 1, vcs.ReviewRequest{ Body: "looks good", Event: vcs.ReviewEventComment, // CommitID intentionally empty }) if err != nil { t.Fatalf("unexpected error: %v", err) } // With empty CommitID and omitempty tag, the field should not appear in JSON if _, exists := gotRawPayload["commit_id"]; exists { t.Errorf("commit_id should be omitted when empty, but was present: %v", gotRawPayload["commit_id"]) } }