Skip to content

Commit

Permalink
Merge pull request #8 from nubank/blob-endpoints
Browse files Browse the repository at this point in the history
Support Git blobs, mirror GitHub API base64 line break
  • Loading branch information
marcobiscaro2112 authored Dec 7, 2024
2 parents dd264a7 + 59ba7a6 commit b08eae5
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 8 deletions.
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=

0 comments on commit b08eae5

Please sign in to comment.