Files
projax/gitea/writeback_test.go
mAi 5a56ad91e5 feat(phase 3h gitea writeback): close/reopen/comment/create from projax
- gitea pkg: CloseIssue, ReopenIssue, CreateIssue, AddComment + ErrForbidden
  classification on 401/403. Client.do sets Content-Type on non-empty bodies.
- web handler: POST /i/{path}/issues/{close|reopen|comment|create}
  - authorisation guard: repo form value must match a gitea-repo item_link
    on the target item (rejects form-crafted writes to unrelated repos)
  - HTMX re-renders issues_section partial after each action
  - busts gitea per-repo cache (open + closed-recent) and dashboard 60s TTL
- templates: ✓ close button + reopen + collapsible comment box on every
  issue row; "+ new issue" disclosure per repo
- design.md §6 retitled "Phase 2.d read; 3h writeback" with auth/perm
  semantics + parked list
- 5 unit tests in gitea/, 5 integration tests in web/ covering happy paths
  + 403 → inline banner fallback
2026-05-15 19:22:11 +02:00

142 lines
4.0 KiB
Go

package gitea
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestCloseIssueRoundTrip(t *testing.T) {
var gotState string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("method = %s, want PATCH", r.Method)
}
if r.Header.Get("Content-Type") != "application/json" {
t.Errorf("Content-Type = %q", r.Header.Get("Content-Type"))
}
var body map[string]string
_ = json.NewDecoder(r.Body).Decode(&body)
gotState = body["state"]
w.WriteHeader(201)
_, _ = io.WriteString(w, `{}`)
}))
defer srv.Close()
c := New(srv.URL, "tok")
if err := c.CloseIssue(context.Background(), "m", "projax", 42); err != nil {
t.Fatalf("CloseIssue: %v", err)
}
if gotState != "closed" {
t.Errorf("upstream got state=%q, want 'closed'", gotState)
}
}
func TestReopenIssueRoundTrip(t *testing.T) {
var gotState string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]string
_ = json.NewDecoder(r.Body).Decode(&body)
gotState = body["state"]
w.WriteHeader(201)
_, _ = io.WriteString(w, `{}`)
}))
defer srv.Close()
c := New(srv.URL, "tok")
if err := c.ReopenIssue(context.Background(), "m", "projax", 42); err != nil {
t.Fatalf("ReopenIssue: %v", err)
}
if gotState != "open" {
t.Errorf("upstream got state=%q, want 'open'", gotState)
}
}
func TestCreateIssueRoundTrip(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("method = %s, want POST", r.Method)
}
if !strings.HasSuffix(r.URL.Path, "/issues") {
t.Errorf("path = %q, want suffix /issues", r.URL.Path)
}
var body map[string]string
_ = json.NewDecoder(r.Body).Decode(&body)
if body["title"] != "Test issue" {
t.Errorf("title = %q", body["title"])
}
w.WriteHeader(201)
_, _ = io.WriteString(w, `{
"number": 99,
"title": "Test issue",
"state": "open",
"updated_at": "2026-05-15T19:00:00Z",
"html_url": "https://mgit.msbls.de/m/projax/issues/99",
"labels": [{"name": "bug"}],
"assignees": [{"login": "mAi"}]
}`)
}))
defer srv.Close()
c := New(srv.URL, "tok")
iss, err := c.CreateIssue(context.Background(), "m", "projax", "Test issue", "body")
if err != nil {
t.Fatalf("CreateIssue: %v", err)
}
if iss.Number != 99 || iss.State != "open" {
t.Errorf("issue = %+v", iss)
}
if len(iss.Labels) != 1 || iss.Labels[0] != "bug" {
t.Errorf("labels = %v", iss.Labels)
}
if len(iss.Assignees) != 1 || iss.Assignees[0] != "mAi" {
t.Errorf("assignees = %v", iss.Assignees)
}
}
func TestAddCommentRoundTrip(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasSuffix(r.URL.Path, "/comments") {
t.Errorf("path = %q", r.URL.Path)
}
var body map[string]string
_ = json.NewDecoder(r.Body).Decode(&body)
if body["body"] != "First!" {
t.Errorf("body = %q", body["body"])
}
w.WriteHeader(201)
_, _ = io.WriteString(w, `{
"id": 12345,
"body": "First!",
"html_url": "https://mgit.msbls.de/m/projax/issues/42#issuecomment-12345",
"created_at": "2026-05-15T19:00:00Z"
}`)
}))
defer srv.Close()
c := New(srv.URL, "tok")
cm, err := c.AddComment(context.Background(), "m", "projax", 42, "First!")
if err != nil {
t.Fatalf("AddComment: %v", err)
}
if cm.ID != 12345 || cm.Body != "First!" {
t.Errorf("comment = %+v", cm)
}
want := time.Date(2026, 5, 15, 19, 0, 0, 0, time.UTC)
if !cm.CreatedAt.Equal(want) {
t.Errorf("CreatedAt = %v, want %v", cm.CreatedAt, want)
}
}
func TestWriteback403ReturnsErrForbidden(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"message":"forbidden"}`, http.StatusForbidden)
}))
defer srv.Close()
c := New(srv.URL, "tok")
if err := c.CloseIssue(context.Background(), "m", "projax", 1); err != ErrForbidden {
t.Errorf("expected ErrForbidden, got %v", err)
}
}