From 31e7c56f33faa0c312bb24cbb3428eb7bfb8f42d Mon Sep 17 00:00:00 2001 From: Marco Biscaro Date: Fri, 6 Dec 2024 19:27:13 -0300 Subject: [PATCH 1/3] Add support for Git blobs https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28 --- src/clj_github_mock/handlers/repos.clj | 12 +++++ src/clj_github_mock/impl/jgit.clj | 12 +++-- test/clj_github_mock/handlers/repos_test.clj | 47 +++++++++++++++++++- test/clj_github_mock/impl/jgit_test.clj | 3 +- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/clj_github_mock/handlers/repos.clj b/src/clj_github_mock/handlers/repos.clj index c77cc41..f9e1ed4 100644 --- a/src/clj_github_mock/handlers/repos.clj +++ b/src/clj_github_mock/handlers/repos.clj @@ -49,6 +49,16 @@ :body body} {:status 404})) +(defn post-blob-handler [{{git-repo :repo/jgit} :repo + body :body}] + {:status 201 + :body (jgit/create-blob! git-repo body)}) + +(defn get-blob-handler [{{git-repo :repo/jgit} :repo + {:keys [sha]} :path-params}] + {:status 200 + :body (jgit/get-blob git-repo sha)}) + (defn post-commit-handler [{{git-repo :repo/jgit} :repo body :body}] {:status 201 @@ -126,6 +136,8 @@ :patch patch-repo-handler}] ["/git/trees" {:post post-tree-handler}] ["/git/trees/:sha" {:get get-tree-handler}] + ["/git/blobs" {:post post-blob-handler}] + ["/git/blobs/:sha" {:get get-blob-handler}] ["/git/commits" {:post post-commit-handler}] ["/git/commits/:sha" {:get get-commit-handler}] ["/git/refs" {:post post-ref-handler}] diff --git a/src/clj_github_mock/impl/jgit.clj b/src/clj_github_mock/impl/jgit.clj index df8618f..4d6c8ae 100644 --- a/src/clj_github_mock/impl/jgit.clj +++ b/src/clj_github_mock/impl/jgit.clj @@ -29,8 +29,11 @@ (let [object-loader (.open reader object-id)] (.getBytes object-loader))) -(defn- insert-blob [^ObjectInserter inserter {:keys [content]}] - (let [^bytes bs (if (bytes? content) content (.getBytes ^String content "UTF-8"))] +(defn- insert-blob [^ObjectInserter inserter {:keys [content encoding]}] + ; https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#create-a-blob + (let [^bytes bs (if (= encoding "base64") + (base64/decode-str->bytes content) + (.getBytes ^String content "UTF-8"))] (.insert inserter Constants/OBJ_BLOB bs))) (defn create-blob! [repo blob] @@ -39,8 +42,10 @@ {:sha (ObjectId/toString object-id)}))) (defn get-blob [repo sha] + ; https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#get-a-blob (let [content (load-object (new-reader repo) (ObjectId/fromString sha))] - {:content (base64/encode-bytes->str content)})) + {:content (base64/encode-bytes->str content) + :encoding "base64"})) (def ^:private github-mode->file-mode {"100644" FileMode/REGULAR_FILE "100755" FileMode/EXECUTABLE_FILE @@ -150,6 +155,7 @@ ; NOTE: when reading the flattened tree, contents are always assumed to be a String ; (needed for backwards compatibility) (update :content #(if (string/blank? %) % (base64/decode-str->str %))) + (assoc :encoding "utf-8") (update :path (partial concat-path base-path)) (dissoc :sha))])) tree))) diff --git a/test/clj_github_mock/handlers/repos_test.clj b/test/clj_github_mock/handlers/repos_test.clj index 4127417..266c4fe 100644 --- a/test/clj_github_mock/handlers/repos_test.clj +++ b/test/clj_github_mock/handlers/repos_test.clj @@ -11,7 +11,8 @@ [malli.core :as m] [matcher-combinators.standalone :refer [match?]] [matcher-combinators.test] - [ring.mock.request :as mock])) + [ring.mock.request :as mock]) + (:import (java.util Arrays))) (defn org-repos-path [org-name] (str "/orgs/" org-name "/repos")) @@ -285,6 +286,50 @@ :content (base64/encode-str->str (:content file))}} (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (-> branch :commit :sha)))))) +(defn create-binary-blob-request [org repo contents] + (let [path (str "/repos/" org "/" repo "/git/blobs") + req (mock/request :post path) + body {:content (base64/encode-bytes->str contents) + :encoding "base64"}] + (assoc req :body body))) + +(defn create-string-blob-request [org repo contents] + (let [path (str "/repos/" org "/" repo "/git/blobs") + req (mock/request :post path) + body {:content contents}] + (assoc req :body body))) + +(defn get-blob-request [org repo sha] + (let [path (str "/repos/" org "/" repo "/git/blobs/" sha) + req (mock/request :get path)] + req)) + +(defspec create-and-get-binary-blob + (prop/for-all + [{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]}) + ^bytes contents gen/bytes] + (let [{create-blob-status :status + {blob-sha :sha} :body} (handler (create-binary-blob-request (:org/name org0) (:repo/name repo0) contents)) + {get-blob-status :status + get-blob-body :body} (handler (get-blob-request (:org/name org0) (:repo/name repo0) blob-sha))] + (and (= 201 create-blob-status) + (= 200 get-blob-status) + (= "base64" (:encoding get-blob-body)) + (Arrays/equals contents (base64/decode-str->bytes (:content get-blob-body))))))) + +(defspec create-and-get-string-blob + (prop/for-all + [{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]}) + contents gen/string] + (let [{create-blob-status :status + {blob-sha :sha} :body} (handler (create-string-blob-request (:org/name org0) (:repo/name repo0) contents)) + {get-blob-status :status + get-blob-body :body} (handler (get-blob-request (:org/name org0) (:repo/name repo0) blob-sha))] + (and (= 201 create-blob-status) + (= 200 get-blob-status) + (= "base64" (:encoding get-blob-body)) + (= contents (base64/decode-str->str (:content get-blob-body))))))) + (defspec get-content-supports-refs (prop/for-all [{:keys [handler org0 repo0 file branch]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) diff --git a/test/clj_github_mock/impl/jgit_test.clj b/test/clj_github_mock/impl/jgit_test.clj index c6f43fe..085c9c8 100644 --- a/test/clj_github_mock/impl/jgit_test.clj +++ b/test/clj_github_mock/impl/jgit_test.clj @@ -27,7 +27,8 @@ (prop/for-all [^bytes content gen/bytes] (let [repo (sut/empty-repo) - {:keys [sha]} (sut/create-blob! repo {:content content})] + {:keys [sha]} (sut/create-blob! repo {:content (base64/encode-bytes->str content) + :encoding "base64"})] (Arrays/equals content (base64/decode-str->bytes (:content (sut/get-blob repo sha))))))) From c424e857f074ab36c13814c507f17e3b2a6b6822 Mon Sep 17 00:00:00 2001 From: Marco Biscaro Date: Fri, 6 Dec 2024 20:32:25 -0300 Subject: [PATCH 2/3] Mirror GitHub API line break behavior in base64 strings Include line breaks every 60 characters in base64 encoded strings to mirror what the actual GitHub API does. --- src/clj_github_mock/impl/base64.clj | 21 ++++++++- src/clj_github_mock/impl/jgit.clj | 2 + test/clj_github_mock/handlers/repos_test.clj | 3 ++ test/clj_github_mock/impl/base64_test.clj | 46 +++++++++++++++++++ test/clj_github_mock/impl/jgit_test.clj | 1 + test/github-mark.png | Bin 0 -> 1316 bytes test/github-png-base64 | 30 ++++++++++++ 7 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 test/clj_github_mock/impl/base64_test.clj create mode 100644 test/github-mark.png create mode 100644 test/github-png-base64 diff --git a/src/clj_github_mock/impl/base64.clj b/src/clj_github_mock/impl/base64.clj index 22dc2c1..8d3e682 100644 --- a/src/clj_github_mock/impl/base64.clj +++ b/src/clj_github_mock/impl/base64.clj @@ -1,4 +1,5 @@ (ns clj-github-mock.impl.base64 + (:require [clojure.string :as str]) (:import (java.nio.charset StandardCharsets) (java.util Base64 Base64$Decoder Base64$Encoder))) @@ -7,11 +8,27 @@ (def ^:private ^Base64$Encoder base64-encoder (Base64/getEncoder)) (def ^:private ^Base64$Decoder base64-decoder (Base64/getDecoder)) +(defn- line-wrap + "Includes line breaks in the provided string `s` every `limit` characters. + + Used to mirror GitHub API's behavior that includes breaks in some + base64-encoded strings." + ^String [s limit] + (->> s + (partition-all limit) + (map str/join) + (str/join "\n"))) + +(defn- unwrap-lines + "Strips line breaks from a base64-encoded string." + ^String [s] + (str/replace s "\n" "")) + (defn encode-bytes->str "Encodes the given byte array to its Base64 representation." ^String [^bytes bs] (let [data (.encode base64-encoder bs)] - (String. data StandardCharsets/UTF_8))) + (line-wrap (String. data StandardCharsets/UTF_8) 60))) (defn encode-str->str "Encodes the given String to its Base64 representation using UTF-8." @@ -21,7 +38,7 @@ (defn decode-str->bytes "Decodes the given Base64 String to a byte array." ^bytes [^String s] - (let [bs (.getBytes s StandardCharsets/UTF_8)] + (let [bs (.getBytes (unwrap-lines s) StandardCharsets/UTF_8)] (.decode base64-decoder bs))) (defn decode-str->str diff --git a/src/clj_github_mock/impl/jgit.clj b/src/clj_github_mock/impl/jgit.clj index 4d6c8ae..b38cc0d 100644 --- a/src/clj_github_mock/impl/jgit.clj +++ b/src/clj_github_mock/impl/jgit.clj @@ -231,6 +231,7 @@ :commit (dissoc commit :sha)}}))) (defn get-content [repo sha path] + ; https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content (let [reader (new-reader repo) commit (RevCommit/parse (load-object reader (ObjectId/fromString sha))) tree-id (-> commit (.getTree) (.getId)) @@ -240,4 +241,5 @@ (let [content (load-object reader object-id)] {:type "file" :path path + :encoding "base64" :content (base64/encode-bytes->str content)})))) diff --git a/test/clj_github_mock/handlers/repos_test.clj b/test/clj_github_mock/handlers/repos_test.clj index 266c4fe..5c8b582 100644 --- a/test/clj_github_mock/handlers/repos_test.clj +++ b/test/clj_github_mock/handlers/repos_test.clj @@ -283,6 +283,7 @@ (= {:status 200 :body {:type "file" :path (:path file) + :encoding "base64" :content (base64/encode-str->str (:content file))}} (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (-> branch :commit :sha)))))) @@ -339,6 +340,7 @@ (= {:status 200 :body {:type "file" :path (:path file) + :encoding "base64" :content (base64/encode-str->str (:content file))}} (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (:name branch)))))) @@ -352,5 +354,6 @@ (= {:status 200 :body {:type "file" :path (:path file) + :encoding "base64" :content (base64/encode-str->str (:content file))}} (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file)))))) diff --git a/test/clj_github_mock/impl/base64_test.clj b/test/clj_github_mock/impl/base64_test.clj new file mode 100644 index 0000000..00b181b --- /dev/null +++ b/test/clj_github_mock/impl/base64_test.clj @@ -0,0 +1,46 @@ +(ns clj-github-mock.impl.base64-test + (:require [clj-github-mock.impl.base64 :as base64] + [clojure.java.io :as io] + [clojure.test :refer :all] + [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.generators :as gen] + [clojure.test.check.properties :as prop]) + (:import (java.util Arrays))) + +(def test-cases + [{:data "" + :encoded ""} + + {:data "Hello world" + :encoded "SGVsbG8gd29ybGQ="} + + {:data "Eclipse Public License - v 2.0\n\n THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE\n PUBLIC LICENSE (\"AGREEMENT\"). ANY USE, REPRODUCTION OR DISTRIBUTION\n OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.\n\n1. DEFINITIONS\n\n\"Contribution\" means:\n\n" + :encoded "RWNsaXBzZSBQdWJsaWMgTGljZW5zZSAtIHYgMi4wCgogICAgVEhFIEFDQ09N\nUEFOWUlORyBQUk9HUkFNIElTIFBST1ZJREVEIFVOREVSIFRIRSBURVJNUyBP\nRiBUSElTIEVDTElQU0UKICAgIFBVQkxJQyBMSUNFTlNFICgiQUdSRUVNRU5U\nIikuIEFOWSBVU0UsIFJFUFJPRFVDVElPTiBPUiBESVNUUklCVVRJT04KICAg\nIE9GIFRIRSBQUk9HUkFNIENPTlNUSVRVVEVTIFJFQ0lQSUVOVCdTIEFDQ0VQ\nVEFOQ0UgT0YgVEhJUyBBR1JFRU1FTlQuCgoxLiBERUZJTklUSU9OUwoKIkNv\nbnRyaWJ1dGlvbiIgbWVhbnM6Cgo="} + + {:data (.readAllBytes (io/input-stream (io/resource "github-mark.png"))) + :encoded (slurp (io/resource "github-png-base64"))}]) + +(deftest base64-tests + (doseq [{:keys [data encoded]} test-cases] + (testing "encoding" + (let [encoder (if (bytes? data) + base64/encode-bytes->str + base64/encode-str->str)] + (is (= encoded (encoder data))))) + + (testing "decoding" + (let [decoder (if (bytes? data) + base64/decode-str->bytes + base64/decode-str->str) + checker (if (bytes? data) + ^[bytes bytes] Arrays/equals + =)] + (is (checker data (decoder encoded))))))) + +(defspec any-byte-array-roundtrips + (prop/for-all [^bytes bs gen/bytes] + (Arrays/equals bs (base64/decode-str->bytes (base64/encode-bytes->str bs))))) + +(defspec any-string-roundtrips + (prop/for-all [s gen/string] + (= s (base64/decode-str->str (base64/encode-str->str s))))) diff --git a/test/clj_github_mock/impl/jgit_test.clj b/test/clj_github_mock/impl/jgit_test.clj index 085c9c8..52c426d 100644 --- a/test/clj_github_mock/impl/jgit_test.clj +++ b/test/clj_github_mock/impl/jgit_test.clj @@ -133,6 +133,7 @@ {:keys [sha]} (sut/create-commit! repo {:tree tree-sha :message "test" :parents []})] (every? #(= {:type "file" :path (:path %) + :encoding "base64" :content (base64/encode-str->str (:content %))} (sut/get-content repo sha (:path %))) tree)))) diff --git a/test/github-mark.png b/test/github-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..aebc1827d9ab63f40b6ed67449199e485dc4d828 GIT binary patch literal 1316 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBDAAG{;hE;^%b*2hb1<+n z3NbJPS&Tr)z$nE4G7ZRL@M4sPvx68lplX;H7}_%#SfFa6fHa7i0V3cu^8!YMi3^zE zss$D>BiJB)b^*u2fD~teM`SSrgLy0nGfFnN0U3;unIRD+5xzcF$@#f@i7EL>sd^Q; z1t47vHWgMtW^QUpqC!P(PF}H9g{=};g%ywu64qBz04piUwpEJo4N!2-FG^J~(=*UB zP_pAvP*AWbN=dT{a&d!d2l8x{GD=Dctn~HE%ggo3jrH=2()A53EiLs8jP#9+bb%^# zi!1X=5-W7`ij^UTz|3(;Elw`VEGWs$&r<-Io0ybeT4JlD1hNPYAnq*5Ohed|R}A$Q z(1ZFQ8GS=N1AVyJK&>_)Q7iwV%v7MwAoJ}EZNMr~#Gv-r=z}araty?$U{Rn~?YM08 z;lXCdB^mdSoq>UAsi%u$h=pKn@Okf$KoPs=_r4!9;)vqeDqtj?n{(ufh>7{-bBp99 zJXW9Buyi%YEsySPe;IYXGlcaH6^K7#{>|w5V#Q;Pb0*s+Cbe{M%$!+${^gwM%z3YP z-tYT-&hq}w`+v{LwlJSg+w3Uf>SuqxI=moh9VhsBxKGME+OHC=rKqC17H?!uvML2 zIitodp847HLj^Y$Fq+l5xW1Ucut9y(yn9*>T%~jBSq0R)QUoUZmCK)@8cM zfN4fRh!qB#H;*$)F0@}x|56f_CwNiv-J za$n+sbEi&}{E&L5FB*0|c?JXjy^UgTV{7NW`P+2F?^OopA79-AA?f9=%RV^%NzHCP zer~60N>#|6nBOaSRqnSp1Ydf(>dMQTjWf!z3X*T_{ZIO2Dn`?U$aN+Vb`H@wuz-dpeYcAI!RT?fahRAJrb;O4@M6%VM8;J;O>l VBj0 Date: Fri, 6 Dec 2024 20:49:25 -0300 Subject: [PATCH 3/3] Version 0.4.0 --- CHANGELOG.md | 4 ++++ project.clj | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f354553..9e413fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog + ## 0.4.0 +- **[BREAKING]** Include line breaks every 60 characters in base64 encoded strings to mirror what the actual GitHub API does +- Add support for Git blobs endpoints (https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#get-a-blob) + ## 0.3.0 - Correctly handle binary files in create-blob! and get-blob operations - Fix reflective accesses in clj-github-mock.impl.jgit diff --git a/project.clj b/project.clj index 781f17f..6006b6e 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject dev.nubank/clj-github-mock "0.3.0" +(defproject dev.nubank/clj-github-mock "0.4.0" :description "An emulator of the github api" :url "https://github.com/nubank/clj-github-mock" :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"