; 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) "
")
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))