0d417e068e
Before posting a review, the bot now: 1. Calls GET /api/v1/user to identify its own login 2. Lists all reviews on the PR 3. Deletes any existing reviews from itself 4. Posts the fresh review This keeps PR threads clean — one review per bot at any time. New Gitea client methods: - GetAuthenticatedUser() — token self-identification - ListReviews() — fetch reviews on a PR - DeleteReview() — delete a review by ID Flag: --update-existing / UPDATE_EXISTING (default true) Set to false to preserve old behavior (stack reviews). All delete failures are non-fatal (logged as warnings). Closes #6
417 lines
13 KiB
Go
417 lines
13 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestGetPullRequest(t *testing.T) {
|
|
pr := PullRequest{
|
|
Title: "Add feature X",
|
|
Body: "This adds feature X.",
|
|
}
|
|
pr.Head.Sha = "abc123"
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/pulls/1" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Header.Get("Authorization") != "token test-token" {
|
|
t.Errorf("unexpected auth header: %s", r.Header.Get("Authorization"))
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(pr)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
got, err := client.GetPullRequest(context.Background(), "owner", "repo", 1)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got.Title != "Add feature X" {
|
|
t.Errorf("expected title %q, got %q", "Add feature X", got.Title)
|
|
}
|
|
if got.Body != "This adds feature X." {
|
|
t.Errorf("expected body %q, got %q", "This adds feature X.", got.Body)
|
|
}
|
|
if got.Head.Sha != "abc123" {
|
|
t.Errorf("expected sha %q, got %q", "abc123", got.Head.Sha)
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequestDiff(t *testing.T) {
|
|
expectedDiff := "diff --git a/file.go b/file.go\n--- a/file.go\n+++ b/file.go\n@@ -1 +1 @@\n-old\n+new\n"
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/pulls/5.diff" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Write([]byte(expectedDiff))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
got, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 5)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != expectedDiff {
|
|
t.Errorf("expected diff %q, got %q", expectedDiff, got)
|
|
}
|
|
}
|
|
|
|
func TestGetCommitStatuses(t *testing.T) {
|
|
statuses := []CommitStatus{
|
|
{Status: "success", Context: "ci/test", Description: "All tests passed"},
|
|
{Status: "failure", Context: "ci/lint", Description: "Lint failed"},
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/commits/abc123/statuses" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(statuses)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
got, err := client.GetCommitStatuses(context.Background(), "owner", "repo", "abc123")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("expected 2 statuses, got %d", len(got))
|
|
}
|
|
if got[0].Status != "success" {
|
|
t.Errorf("expected first status %q, got %q", "success", got[0].Status)
|
|
}
|
|
if got[1].Status != "failure" {
|
|
t.Errorf("expected second status %q, got %q", "failure", got[1].Status)
|
|
}
|
|
}
|
|
|
|
func TestPostReview(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/pulls/3/reviews" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("unexpected content type: %s", r.Header.Get("Content-Type"))
|
|
}
|
|
|
|
var payload struct {
|
|
Body string `json:"body"`
|
|
Event string `json:"event"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("failed to decode payload: %v", err)
|
|
}
|
|
if payload.Body != "LGTM" {
|
|
t.Errorf("expected body %q, got %q", "LGTM", payload.Body)
|
|
}
|
|
if payload.Event != "APPROVED" {
|
|
t.Errorf("expected event %q, got %q", "APPROVED", payload.Event)
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte(`{}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
err := client.PostReview(context.Background(), "owner", "repo", 3, "APPROVED", "LGTM")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequest_Non200(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte(`{"message":"not found"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
_, err := client.GetPullRequest(context.Background(), "owner", "repo", 999)
|
|
if err == nil {
|
|
t.Fatal("expected error for 404, got nil")
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequest_BadJSON(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(`not json`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
_, err := client.GetPullRequest(context.Background(), "owner", "repo", 1)
|
|
if err == nil {
|
|
t.Fatal("expected error for bad JSON, got nil")
|
|
}
|
|
}
|
|
|
|
func TestPostReview_Non200(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"message":"forbidden"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
err := client.PostReview(context.Background(), "owner", "repo", 1, "APPROVED", "test")
|
|
if err == nil {
|
|
t.Fatal("expected error for 403, got nil")
|
|
}
|
|
}
|
|
|
|
func TestGetFileContent(t *testing.T) {
|
|
expected := "# Conventions\n- Be nice\n"
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/raw/CONVENTIONS.md" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Write([]byte(expected))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
got, err := client.GetFileContent(context.Background(), "owner", "repo", "CONVENTIONS.md")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got != expected {
|
|
t.Errorf("expected %q, got %q", expected, got)
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequestFiles(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/pulls/1/files" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`[{"filename":"main.go","status":"modified"},{"filename":"old.go","status":"removed"}]`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
files, err := client.GetPullRequestFiles(context.Background(), "owner", "repo", 1)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(files) != 2 {
|
|
t.Fatalf("expected 2 files, got %d", len(files))
|
|
}
|
|
if files[0].Filename != "main.go" || files[0].Status != "modified" {
|
|
t.Errorf("unexpected first file: %+v", files[0])
|
|
}
|
|
}
|
|
|
|
func TestGetFileContentRef(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/raw/main.go" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.URL.Query().Get("ref") != "feature-branch" {
|
|
t.Errorf("unexpected ref: %s", r.URL.Query().Get("ref"))
|
|
}
|
|
w.Write([]byte("package main\n"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
content, err := client.GetFileContentRef(context.Background(), "owner", "repo", "main.go", "feature-branch")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if content != "package main\n" {
|
|
t.Errorf("unexpected content: %q", content)
|
|
}
|
|
}
|
|
|
|
func TestListContents(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/contents/docs" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
fmt.Fprintf(w, `[{"name":"guide.md","path":"docs/guide.md","type":"file"},{"name":"sub","path":"docs/sub","type":"dir"}]`)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
entries, err := client.ListContents(context.Background(), "owner", "repo", "docs")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(entries) != 2 {
|
|
t.Fatalf("expected 2 entries, got %d", len(entries))
|
|
}
|
|
if entries[0].Type != "file" || entries[0].Path != "docs/guide.md" {
|
|
t.Errorf("unexpected first entry: %+v", entries[0])
|
|
}
|
|
if entries[1].Type != "dir" {
|
|
t.Errorf("expected dir type, got %s", entries[1].Type)
|
|
}
|
|
}
|
|
|
|
func TestGetAllFilesInPath_File(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/v1/repos/owner/repo/contents/README.md" {
|
|
// Gitea returns 404 for contents API on files (it's not a dir)
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if r.URL.Path == "/api/v1/repos/owner/repo/raw/README.md" {
|
|
fmt.Fprintf(w, "# Hello")
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
files, err := client.GetAllFilesInPath(context.Background(), "owner", "repo", "README.md")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(files) != 1 {
|
|
t.Fatalf("expected 1 file, got %d", len(files))
|
|
}
|
|
if files["README.md"] != "# Hello" {
|
|
t.Errorf("unexpected content: %q", files["README.md"])
|
|
}
|
|
}
|
|
|
|
func TestEscapePath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{"simple", "src/main.go", "src/main.go"},
|
|
{"spaces", "my dir/my file.go", "my%20dir/my%20file.go"},
|
|
{"special chars", "path/file#1.txt", "path/file%231.txt"},
|
|
{"empty", "", ""},
|
|
{"single segment", "README.md", "README.md"},
|
|
{"nested deep", "a/b/c/d.md", "a/b/c/d.md"},
|
|
{"already encoded", "path/file%20name.go", "path/file%2520name.go"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := escapePath(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("escapePath(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetAuthenticatedUser(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/user" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Method != "GET" {
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"login":"review-bot","id":42}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
login, err := client.GetAuthenticatedUser(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if login != "review-bot" {
|
|
t.Errorf("expected login %q, got %q", "review-bot", login)
|
|
}
|
|
}
|
|
|
|
func TestGetAuthenticatedUser_EmptyLogin(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"login":"","id":0}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
_, err := client.GetAuthenticatedUser(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error for empty login, got nil")
|
|
}
|
|
}
|
|
|
|
func TestListReviews(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/pulls/5/reviews" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`[{"id":10,"user":{"login":"bot-a"},"state":"APPROVED","stale":false},{"id":11,"user":{"login":"bot-b"},"state":"REQUEST_CHANGES","stale":true}]`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
reviews, err := client.ListReviews(context.Background(), "owner", "repo", 5)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(reviews) != 2 {
|
|
t.Fatalf("expected 2 reviews, got %d", len(reviews))
|
|
}
|
|
if reviews[0].User.Login != "bot-a" {
|
|
t.Errorf("expected bot-a, got %s", reviews[0].User.Login)
|
|
}
|
|
if reviews[1].ID != 11 {
|
|
t.Errorf("expected id 11, got %d", reviews[1].ID)
|
|
}
|
|
}
|
|
|
|
func TestDeleteReview(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v1/repos/owner/repo/pulls/5/reviews/10" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Method != "DELETE" {
|
|
t.Errorf("expected DELETE, got %s", r.Method)
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
err := client.DeleteReview(context.Background(), "owner", "repo", 5, 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteReview_Forbidden(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
w.Write([]byte(`{"message":"forbidden"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewClient(server.URL, "test-token")
|
|
err := client.DeleteReview(context.Background(), "owner", "repo", 5, 10)
|
|
if err == nil {
|
|
t.Fatal("expected error for 403, got nil")
|
|
}
|
|
}
|