; lib/gitea/tests/issues.sx — Phase 4: issue CRUD, comments, labels, ; assignees, content-document bodies (Markdown round-trip + HTML render), ; the derived relations graph, repo-delete purge regression, and the ; issue web routes + JSON API. (st-bootstrap-classes!) (content/bootstrap!) (content-bootstrap-markdown!) (content-bootstrap-table!) (define gitea-issues-pass 0) (define gitea-issues-fail 0) (define gitea-issues-fails (list)) (define gitea-issues-test (fn (name actual expected) (if (= actual expected) (set! gitea-issues-pass (+ gitea-issues-pass 1)) (begin (set! gitea-issues-fail (+ gitea-issues-fail 1)) (set! gitea-issues-fails (append gitea-issues-fails (list {:name name :expected (inspect expected) :actual (inspect actual)}))))))) ; ── helpers ────────────────────────────────────────────────────────── (gitea-issues-test "pad8" (gitea/pad8 7) "00000007") (gitea-issues-test "pad8 wide" (gitea/pad8 12345) "00012345") (gitea-issues-test "digits? yes" (gitea/digits? "123") true) (gitea-issues-test "digits? no" (gitea/digits? "12a") false) (gitea-issues-test "digits? empty" (gitea/digits? "") false) ; ── setup ──────────────────────────────────────────────────────────── (define gi-db (persist/mem-backend)) (define gi-forge (gitea/forge gi-db)) (gitea/user-create! gi-forge "alice") (gitea/user-create! gi-forge "bob") (gitea/user-create! gi-forge "carol") (gitea/user-create! gi-forge "eve") (gitea/repo-create! gi-forge "alice" "proj" {}) (gitea/repo-create! gi-forge "alice" "sec" {:visibility "private"}) (gitea/collab-add! gi-forge "alice" "sec" "bob" "read") (gitea/token-create! gi-forge "alice" "tok-a") (gitea/token-create! gi-forge "bob" "tok-b") (gitea/token-create! gi-forge "eve" "tok-e") ; ── issue CRUD ─────────────────────────────────────────────────────── (define gi-i1 (gitea/issue-create! gi-forge "alice" "proj" "alice" "Crash on boot" "It crashes." {:created-at 10})) (gitea-issues-test "create number" (get gi-i1 :number) 1) (gitea-issues-test "create state" (get gi-i1 :state) "open") (gitea-issues-test "create title" (get gi-i1 :title) "Crash on boot") (gitea-issues-test "create author" (get gi-i1 :author) "alice") (gitea-issues-test "create created-at" (get gi-i1 :created-at) 10) (define gi-i2 (gitea/issue-create! gi-forge "alice" "proj" "bob" "Add docs" "Docs please." {})) (gitea-issues-test "second number" (get gi-i2 :number) 2) (gitea-issues-test "issue-get" (get (gitea/issue-get gi-forge "alice" "proj" 1) :title) "Crash on boot") (gitea-issues-test "issues list" (gitea/issues gi-forge "alice" "proj") (list 1 2)) (gitea-issues-test "issue-records len" (len (gitea/issue-records gi-forge "alice" "proj")) 2) (gitea-issues-test "create on missing repo" (get (gitea/issue-create! gi-forge "alice" "none" "alice" "t" "" {}) :error) "no-such-repo") (gitea-issues-test "create by missing user" (get (gitea/issue-create! gi-forge "alice" "proj" "zeb" "t" "" {}) :error) "no-such-user") (gitea-issues-test "create empty title" (get (gitea/issue-create! gi-forge "alice" "proj" "alice" "" "" {}) :error) "empty-title") (gitea/issue-close! gi-forge "alice" "proj" 2) (gitea-issues-test "close!" (get (gitea/issue-get gi-forge "alice" "proj" 2) :state) "closed") (gitea/issue-reopen! gi-forge "alice" "proj" 2) (gitea-issues-test "reopen!" (get (gitea/issue-get gi-forge "alice" "proj" 2) :state) "open") (gitea-issues-test "close missing" (gitea/issue-close! gi-forge "alice" "proj" 99) nil) (gitea/issue-close! gi-forge "alice" "proj" 2) ; ── comments ───────────────────────────────────────────────────────── (gitea-issues-test "comment author" (get (gitea/issue-comment! gi-forge "alice" "proj" 1 "bob" "Repro *here*." {:at 11}) :author) "bob") (gitea/issue-comment! gi-forge "alice" "proj" 1 "carol" "Same for me." {:at 12}) (gitea-issues-test "comments appended" (len (get (gitea/issue-get gi-forge "alice" "proj" 1) :comments)) 2) (gitea-issues-test "comment order" (get (first (get (gitea/issue-get gi-forge "alice" "proj" 1) :comments)) :body) "Repro *here*.") (gitea-issues-test "comment on missing issue" (get (gitea/issue-comment! gi-forge "alice" "proj" 99 "bob" "x" {}) :error) "no-such-issue") (gitea-issues-test "comment by missing user" (get (gitea/issue-comment! gi-forge "alice" "proj" 1 "zeb" "x" {}) :error) "no-such-user") ; ── labels / assignees ─────────────────────────────────────────────── (gitea/issue-label! gi-forge "alice" "proj" 1 "ui") (gitea/issue-label! gi-forge "alice" "proj" 1 "bug") (gitea-issues-test "labels sorted" (get (gitea/issue-get gi-forge "alice" "proj" 1) :labels) (list "bug" "ui")) (gitea/issue-label! gi-forge "alice" "proj" 1 "bug") (gitea-issues-test "label idempotent" (get (gitea/issue-get gi-forge "alice" "proj" 1) :labels) (list "bug" "ui")) (gitea/issue-unlabel! gi-forge "alice" "proj" 1 "bug") (gitea-issues-test "unlabel" (get (gitea/issue-get gi-forge "alice" "proj" 1) :labels) (list "ui")) (gitea-issues-test "invalid label" (get (gitea/issue-label! gi-forge "alice" "proj" 1 "") :error) "invalid-label") (gitea/issue-assign! gi-forge "alice" "proj" 2 "carol") (gitea-issues-test "assign" (get (gitea/issue-get gi-forge "alice" "proj" 2) :assignees) (list "carol")) (gitea-issues-test "assign unknown user" (get (gitea/issue-assign! gi-forge "alice" "proj" 2 "zeb") :error) "no-such-user") ; ── views ──────────────────────────────────────────────────────────── (gitea-issues-test "issues-open" (len (gitea/issues-open gi-forge "alice" "proj")) 1) (gitea-issues-test "issues-closed" (len (gitea/issues-closed gi-forge "alice" "proj")) 1) (gitea-issues-test "issues-with-label" (map (fn (r) (get r :number)) (gitea/issues-with-label gi-forge "alice" "proj" "ui")) (list 1)) (gitea-issues-test "issues-assigned" (map (fn (r) (get r :number)) (gitea/issues-assigned gi-forge "alice" "proj" "carol")) (list 2)) ; ── content documents ──────────────────────────────────────────────── (define gi-md "# Heading\n\npara text.\n\n```sx\n(+ 1 2)\n```") (define gi-i3 (gitea/issue-create! gi-forge "alice" "proj" "alice" "With md body" gi-md {})) (define gi-doc (gitea/issue-doc "alice" "proj" gi-i3)) (gitea-issues-test "issue doc block count" (content/count gi-doc) 3) (gitea-issues-test "issue doc types" (content/types gi-doc) (list "heading" "text" "code")) (gitea-issues-test "issue html heading" (contains? (gitea/issue-html "alice" "proj" gi-i3) "

Heading

") true) (gitea-issues-test "issue html code block" (contains? (gitea/issue-html "alice" "proj" gi-i3) "
")
  true)

; ── relations graph ──────────────────────────────────────────────────

(gitea-issues-test
  "repo issue nodes"
  (gitea/repo-issue-nodes gi-forge "alice" "proj")
  (list "issue:alice/proj#1" "issue:alice/proj#2" "issue:alice/proj#3"))
(gitea-issues-test
  "authored by alice"
  (gitea/user-authored gi-forge "alice")
  (list "issue:alice/proj#1" "issue:alice/proj#3"))
(gitea-issues-test
  "authored by bob"
  (gitea/user-authored gi-forge "bob")
  (list "issue:alice/proj#2"))
(gitea-issues-test
  "assigned to carol"
  (gitea/user-assigned gi-forge "carol")
  (list "issue:alice/proj#2"))
(gitea-issues-test
  "label issues"
  (gitea/label-issues gi-forge "alice" "proj" "ui")
  (list "issue:alice/proj#1"))
(gitea-issues-test
  "participants incl commenters"
  (gitea/issue-participants gi-forge "alice" "proj" 1)
  (list "user:alice" "user:bob" "user:carol"))
(gitea-issues-test
  "participants author+assignee"
  (gitea/issue-participants gi-forge "alice" "proj" 2)
  (list "user:bob" "user:carol"))

; ── repo delete purges issue state ───────────────────────────────────

(gitea/repo-create! gi-forge "alice" "tmp" {})
(gitea/issue-create! gi-forge "alice" "tmp" "alice" "Ghost?" "" {})
(gitea/collab-add! gi-forge "alice" "tmp" "carol" "write")
(gitea/repo-delete! gi-forge "alice" "tmp")
(gitea/repo-create! gi-forge "alice" "tmp" {})

(gitea-issues-test
  "recreated repo has no ghost issues"
  (gitea/issues gi-forge "alice" "tmp")
  (list))
(gitea-issues-test
  "recreated repo has no ghost collabs"
  (gitea/collabs gi-forge "alice" "tmp")
  (list))
(gitea-issues-test
  "issue numbering restarts"
  (get
    (gitea/issue-create! gi-forge "alice" "tmp" "alice" "Fresh" "" {})
    :number)
  1)
(gitea/repo-delete! gi-forge "alice" "tmp")
(gitea-issues-test
  "deleted repo leaves no issue edges"
  (gitea/repo-issue-nodes gi-forge "alice" "tmp")
  (list))

; ── web routes ───────────────────────────────────────────────────────

(define gi-app (gitea/app gi-forge))
(define gi-hdr (fn (tok) (if (nil? tok) {} {:authorization (str "Bearer " tok)})))
(define
  gi-get
  (fn (target tok) (gi-app (dream-request "GET" target (gi-hdr tok) ""))))
(define
  gi-post
  (fn
    (target tok body)
    (gi-app (dream-request "POST" target (gi-hdr tok) body))))
(define
  gi-put
  (fn
    (target tok body)
    (gi-app (dream-request "PUT" target (gi-hdr tok) body))))
(define
  gi-del
  (fn
    (target tok)
    (gi-app (dream-request "DELETE" target (gi-hdr tok) ""))))

(gitea-issues-test
  "issues page 200"
  (dream-status (gi-get "/alice/proj/issues" nil))
  200)
(gitea-issues-test
  "issues page lists title"
  (contains?
    (dream-resp-body (gi-get "/alice/proj/issues" nil))
    "Crash on boot")
  true)
(gitea-issues-test
  "issues page shows state"
  (contains? (dream-resp-body (gi-get "/alice/proj/issues" nil)) "[closed]")
  true)

(gitea-issues-test
  "issue page 200"
  (dream-status (gi-get "/alice/proj/issues/1" nil))
  200)
(gitea-issues-test
  "issue page shows author"
  (contains? (dream-resp-body (gi-get "/alice/proj/issues/1" nil)) "alice")
  true)
(gitea-issues-test
  "issue page renders body html"
  (contains?
    (dream-resp-body (gi-get "/alice/proj/issues/3" nil))
    "

Heading

") true) (gitea-issues-test "issue page renders comments" (contains? (dream-resp-body (gi-get "/alice/proj/issues/1" nil)) "Same for me.") true) (gitea-issues-test "issue page bad number 404" (dream-status (gi-get "/alice/proj/issues/abc" nil)) 404) (gitea-issues-test "issue page missing 404" (dream-status (gi-get "/alice/proj/issues/99" nil)) 404) (gitea-issues-test "private issues anon 404" (dream-status (gi-get "/alice/sec/issues" nil)) 404) (gitea-issues-test "private issues collab 200" (dream-status (gi-get "/alice/sec/issues" "tok-b")) 200) (gitea-issues-test "api issues len" (len (dream-json-parse (dream-resp-body (gi-get "/api/repos/alice/proj/issues" nil)))) 3) (gitea-issues-test "api issues first number" (get (first (dream-json-parse (dream-resp-body (gi-get "/api/repos/alice/proj/issues" nil)))) :number) 1) (gitea-issues-test "api create anon 401" (dream-status (gi-post "/api/repos/alice/proj/issues" nil (dream-json-encode {:title "t"}))) 401) (gitea-issues-test "api create reader 201" (dream-status (gi-post "/api/repos/alice/proj/issues" "tok-e" (dream-json-encode {:title "From eve" :body "hi"}))) 201) (gitea-issues-test "api created number" (len (gitea/issues gi-forge "alice" "proj")) 4) (gitea-issues-test "api create on private hidden 404" (dream-status (gi-post "/api/repos/alice/sec/issues" "tok-e" (dream-json-encode {:title "x"}))) 404) (gitea-issues-test "api create empty title 400" (dream-status (gi-post "/api/repos/alice/proj/issues" "tok-e" (dream-json-encode {:title ""}))) 400) (gitea-issues-test "api comment 200" (dream-status (gi-post "/api/repos/alice/proj/issues/4/comments" "tok-b" (dream-json-encode {:body "noted"}))) 200) (gitea-issues-test "api comment recorded" (len (get (gitea/issue-get gi-forge "alice" "proj" 4) :comments)) 1) (gitea-issues-test "api comment anon 401" (dream-status (gi-post "/api/repos/alice/proj/issues/4/comments" nil (dream-json-encode {:body "x"}))) 401) (gitea-issues-test "api comment missing issue 404" (dream-status (gi-post "/api/repos/alice/proj/issues/99/comments" "tok-b" (dream-json-encode {:body "x"}))) 404) ; eve authored #4 and may close it without write; reopen as alice (write) (gitea-issues-test "api close by author 200" (dream-status (gi-post "/api/repos/alice/proj/issues/4/close" "tok-e" "{}")) 200) (gitea-issues-test "api close applied" (get (gitea/issue-get gi-forge "alice" "proj" 4) :state) "closed") (gitea-issues-test "api reopen by write 200" (dream-status (gi-post "/api/repos/alice/proj/issues/4/reopen" "tok-a" "{}")) 200) ; issue #5: authored by alice — eve (reader, not author) may not close (gitea/issue-create! gi-forge "alice" "proj" "alice" "Owner issue" "" {}) (gitea-issues-test "api close by stranger 403" (dream-status (gi-post "/api/repos/alice/proj/issues/5/close" "tok-e" "{}")) 403) (gitea-issues-test "api label put by write 200" (dream-status (gi-put "/api/repos/alice/proj/issues/5/labels/bug" "tok-a" "{}")) 200) (gitea-issues-test "api label applied" (get (gitea/issue-get gi-forge "alice" "proj" 5) :labels) (list "bug")) (gitea-issues-test "api label by reader 403" (dream-status (gi-put "/api/repos/alice/proj/issues/5/labels/x" "tok-e" "{}")) 403) (gitea-issues-test "api label delete 200" (dream-status (gi-del "/api/repos/alice/proj/issues/5/labels/bug" "tok-a")) 200) (gitea-issues-test "api label removed" (get (gitea/issue-get gi-forge "alice" "proj" 5) :labels) (list)) (gitea-issues-test "api assign 200" (dream-status (gi-put "/api/repos/alice/proj/issues/5/assignees/bob" "tok-a" "{}")) 200) (gitea-issues-test "api assign applied" (get (gitea/issue-get gi-forge "alice" "proj" 5) :assignees) (list "bob")) (gitea-issues-test "api assign unknown user 400" (dream-status (gi-put "/api/repos/alice/proj/issues/5/assignees/zeb" "tok-a" "{}")) 400) (gitea-issues-test "api unassign 200" (dream-status (gi-del "/api/repos/alice/proj/issues/5/assignees/bob" "tok-a")) 200) (gitea-issues-test "api unassign applied" (get (gitea/issue-get gi-forge "alice" "proj" 5) :assignees) (list))