lib/gitea/repo.sx: forge handle over persist kv; owner principals (user/org directory, identity-backed in Phase 2); repo records with visibility/default-branch metadata; per-repo sx-git namespaces (forge/<owner>/<name>) so delete is a prefix purge; ref resolution (branch/tag/cid, annotated tags peeled) and tree-path navigation. lib/gitea/web.sx: dream routes — repo index, repo home, branches, tree/blob/raw browse at any ref, commit log, single-commit diff view, JSON API for repo create/list/delete (201/400/409 semantics). lib/gitea/tests/repo.sx (91 tests) + conformance.sh + scoreboard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
449 lines
13 KiB
Plaintext
449 lines
13 KiB
Plaintext
; lib/gitea/tests/repo.sx — Phase 1: forge core (owners, repo CRUD, git
|
|
; wiring, ref/tree navigation) and the dream browse views + JSON API.
|
|
|
|
(define gitea-repo-pass 0)
|
|
(define gitea-repo-fail 0)
|
|
(define gitea-repo-fails (list))
|
|
|
|
; compare with = (structural), not equal? — map/filter-derived lists fail
|
|
; equal? against literals even when they print identically
|
|
(define
|
|
gitea-repo-test
|
|
(fn
|
|
(name actual expected)
|
|
(if
|
|
(= actual expected)
|
|
(set! gitea-repo-pass (+ gitea-repo-pass 1))
|
|
(begin
|
|
(set! gitea-repo-fail (+ gitea-repo-fail 1))
|
|
(set! gitea-repo-fails (append gitea-repo-fails (list {:name name :expected (inspect expected) :actual (inspect actual)})))))))
|
|
|
|
(define gt-db (persist/mem-backend))
|
|
(define gt-forge (gitea/forge gt-db))
|
|
|
|
; ── owners ───────────────────────────────────────────────────────────
|
|
|
|
(gitea-repo-test
|
|
"user-create returns user record"
|
|
(get (gitea/user-create! gt-forge "alice") :kind)
|
|
"user")
|
|
(gitea-repo-test
|
|
"org-create returns org record"
|
|
(get (gitea/org-create! gt-forge "acme") :kind)
|
|
"org")
|
|
(gitea-repo-test
|
|
"owner-get finds alice"
|
|
(get (gitea/owner-get gt-forge "alice") :name)
|
|
"alice")
|
|
(gitea-repo-test "owner-exists?" (gitea/owner-exists? gt-forge "alice") true)
|
|
(gitea-repo-test
|
|
"user? on user"
|
|
(gitea/user? (gitea/owner-get gt-forge "alice"))
|
|
true)
|
|
(gitea-repo-test
|
|
"org? on org"
|
|
(gitea/org? (gitea/owner-get gt-forge "acme"))
|
|
true)
|
|
(gitea-repo-test
|
|
"user? on org"
|
|
(gitea/user? (gitea/owner-get gt-forge "acme"))
|
|
false)
|
|
(gitea-repo-test
|
|
"duplicate owner conflicts"
|
|
(get (gitea/user-create! gt-forge "alice") :conflict)
|
|
true)
|
|
(gitea-repo-test
|
|
"owner name with slash rejected"
|
|
(get (gitea/user-create! gt-forge "a/b") :error)
|
|
"invalid-name")
|
|
(gitea-repo-test
|
|
"owner name empty rejected"
|
|
(get (gitea/user-create! gt-forge "") :error)
|
|
"invalid-name")
|
|
(gitea-repo-test
|
|
"reserved owner name rejected"
|
|
(get (gitea/user-create! gt-forge "api") :error)
|
|
"invalid-name")
|
|
(gitea-repo-test
|
|
"owners sorted"
|
|
(gitea/owners gt-forge)
|
|
(list "acme" "alice"))
|
|
|
|
; ── repo CRUD ────────────────────────────────────────────────────────
|
|
|
|
(define gt-rec (gitea/repo-create! gt-forge "alice" "proj" {:description "demo" :created-at 42}))
|
|
|
|
(gitea-repo-test "repo-create owner" (get gt-rec :owner) "alice")
|
|
(gitea-repo-test "repo-create name" (get gt-rec :name) "proj")
|
|
(gitea-repo-test
|
|
"repo-create default visibility"
|
|
(get gt-rec :visibility)
|
|
"public")
|
|
(gitea-repo-test
|
|
"repo-create default branch"
|
|
(get gt-rec :default-branch)
|
|
"main")
|
|
(gitea-repo-test
|
|
"repo-create keeps created-at"
|
|
(get gt-rec :created-at)
|
|
42)
|
|
(gitea-repo-test
|
|
"repo-get description"
|
|
(get (gitea/repo-get gt-forge "alice" "proj") :description)
|
|
"demo")
|
|
(gitea-repo-test
|
|
"repo-exists?"
|
|
(gitea/repo-exists? gt-forge "alice" "proj")
|
|
true)
|
|
(gitea-repo-test
|
|
"repo-get missing"
|
|
(gitea/repo-get gt-forge "alice" "nope")
|
|
nil)
|
|
(gitea-repo-test
|
|
"repo-create unknown owner"
|
|
(get (gitea/repo-create! gt-forge "bob" "x" {}) :error)
|
|
"no-such-owner")
|
|
(gitea-repo-test
|
|
"repo-create duplicate conflicts"
|
|
(get (gitea/repo-create! gt-forge "alice" "proj" {}) :conflict)
|
|
true)
|
|
(gitea-repo-test
|
|
"repo-create bad name"
|
|
(get (gitea/repo-create! gt-forge "alice" "ba d" {}) :error)
|
|
"invalid-name")
|
|
(gitea-repo-test
|
|
"repos lists alice/proj"
|
|
(gitea/repos gt-forge)
|
|
(list "alice/proj"))
|
|
|
|
(gitea/repo-create! gt-forge "acme" "proj" {:visibility "private"})
|
|
|
|
(gitea-repo-test
|
|
"same name under two owners"
|
|
(gitea/repos gt-forge)
|
|
(list "acme/proj" "alice/proj"))
|
|
(gitea-repo-test
|
|
"repos-for alice"
|
|
(gitea/repos-for gt-forge "alice")
|
|
(list "proj"))
|
|
(gitea-repo-test
|
|
"private visibility stored"
|
|
(get (gitea/repo-get gt-forge "acme" "proj") :visibility)
|
|
"private")
|
|
(gitea-repo-test
|
|
"repo-update! description"
|
|
(begin
|
|
(gitea/repo-update!
|
|
gt-forge
|
|
"alice"
|
|
"proj"
|
|
(fn (r) (assoc r :description "rewritten")))
|
|
(get (gitea/repo-get gt-forge "alice" "proj") :description))
|
|
"rewritten")
|
|
(gitea-repo-test
|
|
"repo-update! missing repo"
|
|
(gitea/repo-update! gt-forge "alice" "nope" (fn (r) r))
|
|
nil)
|
|
|
|
; ── git store wiring ─────────────────────────────────────────────────
|
|
|
|
(define gt-grepo (gitea/repo-git gt-forge "alice" "proj"))
|
|
|
|
(gitea-repo-test "new repo HEAD unborn" (git/head gt-grepo) nil)
|
|
(gitea-repo-test
|
|
"new repo HEAD targets main"
|
|
(git/head-target gt-grepo)
|
|
"heads/main")
|
|
(gitea-repo-test "new repo has no branches" (git/branches gt-grepo) (list))
|
|
|
|
(git/add! gt-grepo "README.md" "hello forge")
|
|
(git/add! gt-grepo "src/a.txt" "alpha\n")
|
|
(git/add! gt-grepo "src/b.txt" "beta\n")
|
|
(define gt-c1 (git/commit! gt-grepo {:message "init" :time 1 :author "alice"}))
|
|
|
|
(gitea-repo-test
|
|
"commit! advances main"
|
|
(git/branch-get gt-grepo "main")
|
|
gt-c1)
|
|
|
|
(git/add! gt-grepo "src/a.txt" "alpha2\n")
|
|
(define gt-c2 (git/commit! gt-grepo {:message "tweak a" :time 2 :author "alice"}))
|
|
|
|
(gitea-repo-test
|
|
"log newest first"
|
|
(git/log gt-grepo gt-c2)
|
|
(list gt-c2 gt-c1))
|
|
(gitea-repo-test "branches lists main" (git/branches gt-grepo) (list "main"))
|
|
|
|
(define gt-grepo2 (gitea/repo-git gt-forge "acme" "proj"))
|
|
|
|
(gitea-repo-test
|
|
"objects invisible across repos"
|
|
(git/has? gt-grepo2 gt-c1)
|
|
false)
|
|
|
|
; ── ref resolution ───────────────────────────────────────────────────
|
|
|
|
(gitea-repo-test "resolve branch" (gitea/resolve-ref gt-grepo "main") gt-c2)
|
|
|
|
(git/tag-lightweight! gt-grepo "v1")
|
|
(gitea-repo-test
|
|
"resolve lightweight tag"
|
|
(gitea/resolve-ref gt-grepo "v1")
|
|
gt-c2)
|
|
|
|
(git/tag! gt-grepo "v2" {:message "release" :time 3})
|
|
(gitea-repo-test
|
|
"resolve annotated tag peels to commit"
|
|
(gitea/resolve-ref gt-grepo "v2")
|
|
gt-c2)
|
|
|
|
(gitea-repo-test "resolve raw cid" (gitea/resolve-ref gt-grepo gt-c1) gt-c1)
|
|
(gitea-repo-test
|
|
"resolve unknown ref"
|
|
(gitea/resolve-ref gt-grepo "nope")
|
|
nil)
|
|
|
|
; ── tree navigation ──────────────────────────────────────────────────
|
|
|
|
(gitea-repo-test
|
|
"tree-at root is tree"
|
|
(get (gitea/tree-at gt-grepo gt-c2 "") :kind)
|
|
"tree")
|
|
(gitea-repo-test
|
|
"tree-at file is blob"
|
|
(get (gitea/tree-at gt-grepo gt-c2 "src/a.txt") :kind)
|
|
"blob")
|
|
(gitea-repo-test
|
|
"tree-at file cid matches content"
|
|
(get (gitea/tree-at gt-grepo gt-c2 "src/a.txt") :cid)
|
|
(git/cid (git/blob "alpha2\n")))
|
|
(gitea-repo-test
|
|
"tree-at dir is tree"
|
|
(get (gitea/tree-at gt-grepo gt-c2 "src") :kind)
|
|
"tree")
|
|
(gitea-repo-test
|
|
"tree-at missing path"
|
|
(gitea/tree-at gt-grepo gt-c2 "src/zzz")
|
|
nil)
|
|
(gitea-repo-test
|
|
"tree-at path through blob"
|
|
(gitea/tree-at gt-grepo gt-c2 "README.md/x")
|
|
nil)
|
|
(gitea-repo-test
|
|
"tree-at non-commit cid"
|
|
(gitea/tree-at gt-grepo (git/cid (git/blob "alpha2\n")) "")
|
|
nil)
|
|
|
|
; ── browse views ─────────────────────────────────────────────────────
|
|
|
|
(define gt-app (gitea/app gt-forge))
|
|
(define
|
|
gt-get
|
|
(fn (target) (gt-app (dream-request "GET" target {} ""))))
|
|
(define
|
|
gt-post
|
|
(fn (target body) (gt-app (dream-request "POST" target {} body))))
|
|
(define
|
|
gt-del
|
|
(fn (target) (gt-app (dream-request "DELETE" target {} ""))))
|
|
|
|
(gitea-repo-test "GET / status" (dream-status (gt-get "/")) 200)
|
|
(gitea-repo-test
|
|
"GET / lists repos"
|
|
(contains? (dream-resp-body (gt-get "/")) "alice/proj")
|
|
true)
|
|
|
|
(gitea-repo-test
|
|
"repo home status"
|
|
(dream-status (gt-get "/alice/proj"))
|
|
200)
|
|
(gitea-repo-test
|
|
"repo home shows description"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj")) "rewritten")
|
|
true)
|
|
(gitea-repo-test
|
|
"repo home shows branch"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj")) "main")
|
|
true)
|
|
(gitea-repo-test
|
|
"empty repo home"
|
|
(contains? (dream-resp-body (gt-get "/acme/proj")) "empty repository")
|
|
true)
|
|
(gitea-repo-test
|
|
"unknown repo 404"
|
|
(dream-status (gt-get "/nobody/none"))
|
|
404)
|
|
|
|
(gitea-repo-test
|
|
"branches page lists main"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj/branches")) "main")
|
|
true)
|
|
(gitea-repo-test
|
|
"branches page unknown repo 404"
|
|
(dream-status (gt-get "/nobody/none/branches"))
|
|
404)
|
|
|
|
(gitea-repo-test
|
|
"tree root status"
|
|
(dream-status (gt-get "/alice/proj/tree/main"))
|
|
200)
|
|
(gitea-repo-test
|
|
"tree root lists src"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj/tree/main")) "src")
|
|
true)
|
|
(gitea-repo-test
|
|
"tree root lists README"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj/tree/main")) "README.md")
|
|
true)
|
|
(gitea-repo-test
|
|
"tree subdir lists a.txt"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj/tree/main/src")) "a.txt")
|
|
true)
|
|
(gitea-repo-test
|
|
"tree at tag"
|
|
(dream-status (gt-get "/alice/proj/tree/v1"))
|
|
200)
|
|
(gitea-repo-test
|
|
"tree bad ref 404"
|
|
(dream-status (gt-get "/alice/proj/tree/nope"))
|
|
404)
|
|
(gitea-repo-test
|
|
"tree on blob path 404"
|
|
(dream-status (gt-get "/alice/proj/tree/main/README.md"))
|
|
404)
|
|
|
|
(gitea-repo-test
|
|
"blob status"
|
|
(dream-status (gt-get "/alice/proj/blob/main/src/a.txt"))
|
|
200)
|
|
(gitea-repo-test
|
|
"blob shows content"
|
|
(contains?
|
|
(dream-resp-body (gt-get "/alice/proj/blob/main/src/a.txt"))
|
|
"alpha2")
|
|
true)
|
|
(gitea-repo-test
|
|
"blob on tree path 404"
|
|
(dream-status (gt-get "/alice/proj/blob/main/src"))
|
|
404)
|
|
(gitea-repo-test
|
|
"raw body exact"
|
|
(dream-resp-body (gt-get "/alice/proj/raw/main/src/a.txt"))
|
|
"alpha2\n")
|
|
(gitea-repo-test
|
|
"raw missing file 404"
|
|
(dream-status (gt-get "/alice/proj/raw/main/zzz"))
|
|
404)
|
|
|
|
(gitea-repo-test
|
|
"commits status"
|
|
(dream-status (gt-get "/alice/proj/commits/main"))
|
|
200)
|
|
(gitea-repo-test
|
|
"commits show newest message"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj/commits/main")) "tweak a")
|
|
true)
|
|
(gitea-repo-test
|
|
"commits show oldest message"
|
|
(contains? (dream-resp-body (gt-get "/alice/proj/commits/main")) "init")
|
|
true)
|
|
(gitea-repo-test
|
|
"commits bad ref 404"
|
|
(dream-status (gt-get "/alice/proj/commits/nope"))
|
|
404)
|
|
|
|
(gitea-repo-test
|
|
"commit view message"
|
|
(contains?
|
|
(dream-resp-body (gt-get (str "/alice/proj/commit/" gt-c2)))
|
|
"tweak a")
|
|
true)
|
|
(gitea-repo-test
|
|
"commit view diff content"
|
|
(contains?
|
|
(dream-resp-body (gt-get (str "/alice/proj/commit/" gt-c2)))
|
|
"alpha2")
|
|
true)
|
|
(gitea-repo-test
|
|
"root commit lists files"
|
|
(contains?
|
|
(dream-resp-body (gt-get (str "/alice/proj/commit/" gt-c1)))
|
|
"README.md")
|
|
true)
|
|
(gitea-repo-test
|
|
"commit bad cid 404"
|
|
(dream-status (gt-get "/alice/proj/commit/zzz"))
|
|
404)
|
|
|
|
; ── json api ─────────────────────────────────────────────────────────
|
|
|
|
(gitea-repo-test
|
|
"api repos json"
|
|
(dream-json-parse (dream-resp-body (gt-get "/api/repos")))
|
|
(list "acme/proj" "alice/proj"))
|
|
|
|
(gitea-repo-test
|
|
"api create 201"
|
|
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "web" :owner "alice"})))
|
|
201)
|
|
(gitea-repo-test
|
|
"api create persisted"
|
|
(gitea/repo-exists? gt-forge "alice" "web")
|
|
true)
|
|
(gitea-repo-test
|
|
"api create duplicate 409"
|
|
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "web" :owner "alice"})))
|
|
409)
|
|
(gitea-repo-test
|
|
"api create unknown owner 400"
|
|
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "web" :owner "zeb"})))
|
|
400)
|
|
(gitea-repo-test
|
|
"api create bad name 400"
|
|
(dream-status (gt-post "/api/repos" (dream-json-encode {:name "b d" :owner "alice"})))
|
|
400)
|
|
(gitea-repo-test
|
|
"api delete 200"
|
|
(dream-status (gt-del "/api/repos/alice/web"))
|
|
200)
|
|
(gitea-repo-test
|
|
"api delete gone"
|
|
(gitea/repo-exists? gt-forge "alice" "web")
|
|
false)
|
|
(gitea-repo-test
|
|
"api delete missing 404"
|
|
(dream-status (gt-del "/api/repos/alice/web"))
|
|
404)
|
|
|
|
; ── delete purges the git namespace ──────────────────────────────────
|
|
|
|
(gitea/repo-create! gt-forge "alice" "tmp" {})
|
|
(define gt-gtmp (gitea/repo-git gt-forge "alice" "tmp"))
|
|
(git/add! gt-gtmp "f.txt" "data")
|
|
(git/commit! gt-gtmp {:message "x" :time 9})
|
|
|
|
(gitea-repo-test
|
|
"delete returns true"
|
|
(gitea/repo-delete! gt-forge "alice" "tmp")
|
|
true)
|
|
(gitea-repo-test
|
|
"delete removes record"
|
|
(gitea/repo-get gt-forge "alice" "tmp")
|
|
nil)
|
|
(gitea-repo-test
|
|
"delete purges git keys"
|
|
(len
|
|
(filter
|
|
(fn (k) (starts-with? k "forge/alice/tmp/"))
|
|
(persist/kv-keys gt-db)))
|
|
0)
|
|
(gitea-repo-test
|
|
"delete missing returns false"
|
|
(gitea/repo-delete! gt-forge "alice" "tmp")
|
|
false)
|
|
(gitea-repo-test
|
|
"other repos survive delete"
|
|
(gitea/repos gt-forge)
|
|
(list "acme/proj" "alice/proj"))
|