Skip to content

Commit b08eae5

Browse files
Merge pull request #8 from nubank/blob-endpoints
Support Git blobs, mirror GitHub API base64 line break
2 parents dd264a7 + 59ba7a6 commit b08eae5

File tree

10 files changed

+175
-8
lines changed

10 files changed

+175
-8
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 0.4.0
4+
- **[BREAKING]** Include line breaks every 60 characters in base64 encoded strings to mirror what the actual GitHub API does
5+
- Add support for Git blobs endpoints (https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#get-a-blob)
6+
37
## 0.3.0
48
- Correctly handle binary files in create-blob! and get-blob operations
59
- Fix reflective accesses in clj-github-mock.impl.jgit

project.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(defproject dev.nubank/clj-github-mock "0.3.0"
1+
(defproject dev.nubank/clj-github-mock "0.4.0"
22
:description "An emulator of the github api"
33
:url "https://github.com/nubank/clj-github-mock"
44
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"

src/clj_github_mock/handlers/repos.clj

+12
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@
4949
:body body}
5050
{:status 404}))
5151

52+
(defn post-blob-handler [{{git-repo :repo/jgit} :repo
53+
body :body}]
54+
{:status 201
55+
:body (jgit/create-blob! git-repo body)})
56+
57+
(defn get-blob-handler [{{git-repo :repo/jgit} :repo
58+
{:keys [sha]} :path-params}]
59+
{:status 200
60+
:body (jgit/get-blob git-repo sha)})
61+
5262
(defn post-commit-handler [{{git-repo :repo/jgit} :repo
5363
body :body}]
5464
{:status 201
@@ -126,6 +136,8 @@
126136
:patch patch-repo-handler}]
127137
["/git/trees" {:post post-tree-handler}]
128138
["/git/trees/:sha" {:get get-tree-handler}]
139+
["/git/blobs" {:post post-blob-handler}]
140+
["/git/blobs/:sha" {:get get-blob-handler}]
129141
["/git/commits" {:post post-commit-handler}]
130142
["/git/commits/:sha" {:get get-commit-handler}]
131143
["/git/refs" {:post post-ref-handler}]

src/clj_github_mock/impl/base64.clj

+19-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
(ns clj-github-mock.impl.base64
2+
(:require [clojure.string :as str])
23
(:import (java.nio.charset StandardCharsets)
34
(java.util Base64 Base64$Decoder Base64$Encoder)))
45

@@ -7,11 +8,27 @@
78
(def ^:private ^Base64$Encoder base64-encoder (Base64/getEncoder))
89
(def ^:private ^Base64$Decoder base64-decoder (Base64/getDecoder))
910

11+
(defn- line-wrap
12+
"Includes line breaks in the provided string `s` every `limit` characters.
13+
14+
Used to mirror GitHub API's behavior that includes breaks in some
15+
base64-encoded strings."
16+
^String [s limit]
17+
(->> s
18+
(partition-all limit)
19+
(map str/join)
20+
(str/join "\n")))
21+
22+
(defn- unwrap-lines
23+
"Strips line breaks from a base64-encoded string."
24+
^String [s]
25+
(str/replace s "\n" ""))
26+
1027
(defn encode-bytes->str
1128
"Encodes the given byte array to its Base64 representation."
1229
^String [^bytes bs]
1330
(let [data (.encode base64-encoder bs)]
14-
(String. data StandardCharsets/UTF_8)))
31+
(line-wrap (String. data StandardCharsets/UTF_8) 60)))
1532

1633
(defn encode-str->str
1734
"Encodes the given String to its Base64 representation using UTF-8."
@@ -21,7 +38,7 @@
2138
(defn decode-str->bytes
2239
"Decodes the given Base64 String to a byte array."
2340
^bytes [^String s]
24-
(let [bs (.getBytes s StandardCharsets/UTF_8)]
41+
(let [bs (.getBytes (unwrap-lines s) StandardCharsets/UTF_8)]
2542
(.decode base64-decoder bs)))
2643

2744
(defn decode-str->str

src/clj_github_mock/impl/jgit.clj

+11-3
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@
2929
(let [object-loader (.open reader object-id)]
3030
(.getBytes object-loader)))
3131

32-
(defn- insert-blob [^ObjectInserter inserter {:keys [content]}]
33-
(let [^bytes bs (if (bytes? content) content (.getBytes ^String content "UTF-8"))]
32+
(defn- insert-blob [^ObjectInserter inserter {:keys [content encoding]}]
33+
; https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#create-a-blob
34+
(let [^bytes bs (if (= encoding "base64")
35+
(base64/decode-str->bytes content)
36+
(.getBytes ^String content "UTF-8"))]
3437
(.insert inserter Constants/OBJ_BLOB bs)))
3538

3639
(defn create-blob! [repo blob]
@@ -39,8 +42,10 @@
3942
{:sha (ObjectId/toString object-id)})))
4043

4144
(defn get-blob [repo sha]
45+
; https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#get-a-blob
4246
(let [content (load-object (new-reader repo) (ObjectId/fromString sha))]
43-
{:content (base64/encode-bytes->str content)}))
47+
{:content (base64/encode-bytes->str content)
48+
:encoding "base64"}))
4449

4550
(def ^:private github-mode->file-mode {"100644" FileMode/REGULAR_FILE
4651
"100755" FileMode/EXECUTABLE_FILE
@@ -150,6 +155,7 @@
150155
; NOTE: when reading the flattened tree, contents are always assumed to be a String
151156
; (needed for backwards compatibility)
152157
(update :content #(if (string/blank? %) % (base64/decode-str->str %)))
158+
(assoc :encoding "utf-8")
153159
(update :path (partial concat-path base-path))
154160
(dissoc :sha))]))
155161
tree)))
@@ -225,6 +231,7 @@
225231
:commit (dissoc commit :sha)}})))
226232

227233
(defn get-content [repo sha path]
234+
; https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content
228235
(let [reader (new-reader repo)
229236
commit (RevCommit/parse (load-object reader (ObjectId/fromString sha)))
230237
tree-id (-> commit (.getTree) (.getId))
@@ -234,4 +241,5 @@
234241
(let [content (load-object reader object-id)]
235242
{:type "file"
236243
:path path
244+
:encoding "base64"
237245
:content (base64/encode-bytes->str content)}))))

test/clj_github_mock/handlers/repos_test.clj

+49-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
[malli.core :as m]
1212
[matcher-combinators.standalone :refer [match?]]
1313
[matcher-combinators.test]
14-
[ring.mock.request :as mock]))
14+
[ring.mock.request :as mock])
15+
(:import (java.util Arrays)))
1516

1617
(defn org-repos-path [org-name]
1718
(str "/orgs/" org-name "/repos"))
@@ -282,9 +283,54 @@
282283
(= {:status 200
283284
:body {:type "file"
284285
:path (:path file)
286+
:encoding "base64"
285287
:content (base64/encode-str->str (:content file))}}
286288
(handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (-> branch :commit :sha))))))
287289

290+
(defn create-binary-blob-request [org repo contents]
291+
(let [path (str "/repos/" org "/" repo "/git/blobs")
292+
req (mock/request :post path)
293+
body {:content (base64/encode-bytes->str contents)
294+
:encoding "base64"}]
295+
(assoc req :body body)))
296+
297+
(defn create-string-blob-request [org repo contents]
298+
(let [path (str "/repos/" org "/" repo "/git/blobs")
299+
req (mock/request :post path)
300+
body {:content contents}]
301+
(assoc req :body body)))
302+
303+
(defn get-blob-request [org repo sha]
304+
(let [path (str "/repos/" org "/" repo "/git/blobs/" sha)
305+
req (mock/request :get path)]
306+
req))
307+
308+
(defspec create-and-get-binary-blob
309+
(prop/for-all
310+
[{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]})
311+
^bytes contents gen/bytes]
312+
(let [{create-blob-status :status
313+
{blob-sha :sha} :body} (handler (create-binary-blob-request (:org/name org0) (:repo/name repo0) contents))
314+
{get-blob-status :status
315+
get-blob-body :body} (handler (get-blob-request (:org/name org0) (:repo/name repo0) blob-sha))]
316+
(and (= 201 create-blob-status)
317+
(= 200 get-blob-status)
318+
(= "base64" (:encoding get-blob-body))
319+
(Arrays/equals contents (base64/decode-str->bytes (:content get-blob-body)))))))
320+
321+
(defspec create-and-get-string-blob
322+
(prop/for-all
323+
[{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]})
324+
contents gen/string]
325+
(let [{create-blob-status :status
326+
{blob-sha :sha} :body} (handler (create-string-blob-request (:org/name org0) (:repo/name repo0) contents))
327+
{get-blob-status :status
328+
get-blob-body :body} (handler (get-blob-request (:org/name org0) (:repo/name repo0) blob-sha))]
329+
(and (= 201 create-blob-status)
330+
(= 200 get-blob-status)
331+
(= "base64" (:encoding get-blob-body))
332+
(= contents (base64/decode-str->str (:content get-blob-body)))))))
333+
288334
(defspec get-content-supports-refs
289335
(prop/for-all
290336
[{:keys [handler org0 repo0 file branch]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]})
@@ -294,6 +340,7 @@
294340
(= {:status 200
295341
:body {:type "file"
296342
:path (:path file)
343+
:encoding "base64"
297344
:content (base64/encode-str->str (:content file))}}
298345
(handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (:name branch))))))
299346

@@ -307,5 +354,6 @@
307354
(= {:status 200
308355
:body {:type "file"
309356
:path (:path file)
357+
:encoding "base64"
310358
:content (base64/encode-str->str (:content file))}}
311359
(handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file))))))
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
(ns clj-github-mock.impl.base64-test
2+
(:require [clj-github-mock.impl.base64 :as base64]
3+
[clojure.java.io :as io]
4+
[clojure.test :refer :all]
5+
[clojure.test.check.clojure-test :refer [defspec]]
6+
[clojure.test.check.generators :as gen]
7+
[clojure.test.check.properties :as prop])
8+
(:import (java.util Arrays)))
9+
10+
(def test-cases
11+
[{:data ""
12+
:encoded ""}
13+
14+
{:data "Hello world"
15+
:encoded "SGVsbG8gd29ybGQ="}
16+
17+
{: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"
18+
:encoded "RWNsaXBzZSBQdWJsaWMgTGljZW5zZSAtIHYgMi4wCgogICAgVEhFIEFDQ09N\nUEFOWUlORyBQUk9HUkFNIElTIFBST1ZJREVEIFVOREVSIFRIRSBURVJNUyBP\nRiBUSElTIEVDTElQU0UKICAgIFBVQkxJQyBMSUNFTlNFICgiQUdSRUVNRU5U\nIikuIEFOWSBVU0UsIFJFUFJPRFVDVElPTiBPUiBESVNUUklCVVRJT04KICAg\nIE9GIFRIRSBQUk9HUkFNIENPTlNUSVRVVEVTIFJFQ0lQSUVOVCdTIEFDQ0VQ\nVEFOQ0UgT0YgVEhJUyBBR1JFRU1FTlQuCgoxLiBERUZJTklUSU9OUwoKIkNv\nbnRyaWJ1dGlvbiIgbWVhbnM6Cgo="}
19+
20+
{:data (.readAllBytes (io/input-stream (io/resource "github-mark.png")))
21+
:encoded (slurp (io/resource "github-png-base64"))}])
22+
23+
(deftest base64-tests
24+
(doseq [{:keys [data encoded]} test-cases]
25+
(testing "encoding"
26+
(let [encoder (if (bytes? data)
27+
base64/encode-bytes->str
28+
base64/encode-str->str)]
29+
(is (= encoded (encoder data)))))
30+
31+
(testing "decoding"
32+
(let [decoder (if (bytes? data)
33+
base64/decode-str->bytes
34+
base64/decode-str->str)
35+
checker (if (bytes? data)
36+
^[bytes bytes] Arrays/equals
37+
=)]
38+
(is (checker data (decoder encoded)))))))
39+
40+
(defspec any-byte-array-roundtrips
41+
(prop/for-all [^bytes bs gen/bytes]
42+
(Arrays/equals bs (base64/decode-str->bytes (base64/encode-bytes->str bs)))))
43+
44+
(defspec any-string-roundtrips
45+
(prop/for-all [s gen/string]
46+
(= s (base64/decode-str->str (base64/encode-str->str s)))))

test/clj_github_mock/impl/jgit_test.clj

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
(prop/for-all
2828
[^bytes content gen/bytes]
2929
(let [repo (sut/empty-repo)
30-
{:keys [sha]} (sut/create-blob! repo {:content content})]
30+
{:keys [sha]} (sut/create-blob! repo {:content (base64/encode-bytes->str content)
31+
:encoding "base64"})]
3132
(Arrays/equals content
3233
(base64/decode-str->bytes (:content (sut/get-blob repo sha)))))))
3334

@@ -132,6 +133,7 @@
132133
{:keys [sha]} (sut/create-commit! repo {:tree tree-sha :message "test" :parents []})]
133134
(every? #(= {:type "file"
134135
:path (:path %)
136+
:encoding "base64"
135137
:content (base64/encode-str->str (:content %))}
136138
(sut/get-content repo sha (:path %)))
137139
tree))))

test/github-mark.png

1.29 KB
Loading

test/github-png-base64

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGP
2+
C/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3Cc
3+
ulE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABK
4+
ARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAWgA
5+
AAABAAABaAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAAB
6+
AAAAEAAAAAA+UMZWAAAACXBIWXMAADddAAA3XQEZgEZdAAABWWlUWHRYTUw6
7+
Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpu
8+
czptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJE
9+
RiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRm
10+
LXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
11+
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUu
12+
Y29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8
13+
L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgog
14+
ICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAACpUlEQVQ4EX1Tz0tU
15+
URQ+5973xjIIWgy1EDIbbWzE1BQ0N9POoh8YSKvIsKWrCNpIi7b9AS1LaBMu
16+
wnAX4gP7AUnoqOMozjS2kWKEiAiZmXvP6ZyXA27qDO++884537nf/c4dhAPL
17+
ZrNBFEVOP8909p4ODA8TcRsAsjFQdoTvy4Xlr5o/XIuHA62ZzKmAgqcSu22s
18+
DTXXMPK+Lv4rb/3D8vr690YTbDgdmZ4B2XEhDBPH6vWa4vblseqIeXmOSg4k
19+
95sMXv6SX1lSbMxAKVugbQAWAL6R4ivW2iYiUjAYY8ALAymeY4BrGvNg2vVI
20+
Rj8s+GdBGCrlxWIhNwoIF8nRPSa8xExDztG4NBnYLuRuAcO81ipGsdh2rrff
21+
Wl4CRGDiz8XCSr8m/mWp8xfeIZphZgbyOBCgoRFEK43lhzytwEwmk0gmkxRF
22+
SWGsqlewUqmYfD5fA+bnwnDYoAE2fkRF6JAAsCeynt4qQAp1nH8FECeKZBEp
23+
dAnYzDuiKopGijWyNMZlOLRHtOh/RuS1Jp6OYg0j7AotUVpizIMKbmkZapJX
24+
vKN+i+FBDGSTQRE0UIxiMZXpvmHAzsrIdPga7i8VVtdjmAAP3rEW7V19nez9
25+
gsRO6mgJ/E0twFRnd0Wm8EH8ZWuDx865RWR6VNxc+6gNJN+HgFPS5arU6dmB
26+
mHdKXR0ppcly3SfCIHEdGT957yZkoM2Edk/BsaFkrB0VPyEca7q72AOYmfEm
27+
K9exuLE2W3e1J0J4Tgj9QmPun2i2O1qlFnLwTf4LP1BMGCacp6lSIfcaxsZs
28+
44yqqj+b7rkr430RBAFUq7V0eWt1Sxuk0z2tZLEsOv2UTSaLG7mXElYa1FDa
29+
a7fS5sq09fvHnavfSUB1V8Fq1tb3vOfxJuPaYrDUSji+J38AqR4yTd6zmh4A
30+
AAAASUVORK5CYII=

0 commit comments

Comments
 (0)