- 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
142 lines
4.0 KiB
Go
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)
|
|
}
|
|
}
|