Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Git blobs, mirror GitHub API base64 line break #8

Merged
merged 3 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
12 changes: 12 additions & 0 deletions src/clj_github_mock/handlers/repos.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}]
Expand Down
21 changes: 19 additions & 2 deletions src/clj_github_mock/impl/base64.clj
Original file line number Diff line number Diff line change
@@ -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)))

Expand All @@ -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."
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/clj_github_mock/impl/jgit.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -225,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))
Expand All @@ -234,4 +241,5 @@
(let [content (load-object reader object-id)]
{:type "file"
:path path
:encoding "base64"
:content (base64/encode-bytes->str content)}))))
50 changes: 49 additions & 1 deletion test/clj_github_mock/handlers/repos_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -282,9 +283,54 @@
(= {: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))))))

(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]]})
Expand All @@ -294,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))))))

Expand All @@ -307,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))))))
46 changes: 46 additions & 0 deletions test/clj_github_mock/impl/base64_test.clj
Original file line number Diff line number Diff line change
@@ -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)))))
4 changes: 3 additions & 1 deletion test/clj_github_mock/impl/jgit_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))))))

Expand Down Expand Up @@ -132,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))))
Binary file added test/github-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions test/github-png-base64
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGP
C/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3Cc
ulE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABK
ARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAWgA
AAABAAABaAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAAB
AAAAEAAAAAA+UMZWAAAACXBIWXMAADddAAA3XQEZgEZdAAABWWlUWHRYTUw6
Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpu
czptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJE
RiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRm
LXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91
dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUu
Y29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8
L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgog
ICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgoZXuEHAAACpUlEQVQ4EX1Tz0tU
URQ+5973xjIIWgy1EDIbbWzE1BQ0N9POoh8YSKvIsKWrCNpIi7b9AS1LaBMu
wnAX4gP7AUnoqOMozjS2kWKEiAiZmXvP6ZyXA27qDO++884537nf/c4dhAPL
ZrNBFEVOP8909p4ODA8TcRsAsjFQdoTvy4Xlr5o/XIuHA62ZzKmAgqcSu22s
DTXXMPK+Lv4rb/3D8vr690YTbDgdmZ4B2XEhDBPH6vWa4vblseqIeXmOSg4k
95sMXv6SX1lSbMxAKVugbQAWAL6R4ivW2iYiUjAYY8ALAymeY4BrGvNg2vVI
Rj8s+GdBGCrlxWIhNwoIF8nRPSa8xExDztG4NBnYLuRuAcO81ipGsdh2rrff
Wl4CRGDiz8XCSr8m/mWp8xfeIZphZgbyOBCgoRFEK43lhzytwEwmk0gmkxRF
SWGsqlewUqmYfD5fA+bnwnDYoAE2fkRF6JAAsCeynt4qQAp1nH8FECeKZBEp
dAnYzDuiKopGijWyNMZlOLRHtOh/RuS1Jp6OYg0j7AotUVpizIMKbmkZapJX
vKN+i+FBDGSTQRE0UIxiMZXpvmHAzsrIdPga7i8VVtdjmAAP3rEW7V19nez9
gsRO6mgJ/E0twFRnd0Wm8EH8ZWuDx865RWR6VNxc+6gNJN+HgFPS5arU6dmB
mHdKXR0ppcly3SfCIHEdGT957yZkoM2Edk/BsaFkrB0VPyEca7q72AOYmfEm
K9exuLE2W3e1J0J4Tgj9QmPun2i2O1qlFnLwTf4LP1BMGCacp6lSIfcaxsZs
44yqqj+b7rkr430RBAFUq7V0eWt1Sxuk0z2tZLEsOv2UTSaLG7mXElYa1FDa
a7fS5sq09fvHnavfSUB1V8Fq1tb3vOfxJuPaYrDUSji+J38AqR4yTd6zmh4A
AAAASUVORK5CYII=
Loading