├── .github └── workflows │ ├── clojure.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── project.clj ├── release.sh ├── src └── clj_github_mock │ ├── core.clj │ ├── handlers │ └── repos.clj │ └── impl │ ├── base64.clj │ ├── database.clj │ └── jgit.clj └── test ├── clj_github_mock ├── generators.clj ├── handlers │ └── repos_test.clj └── impl │ ├── base64_test.clj │ └── jgit_test.clj ├── github-mark.png └── github-png-base64 /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | test-clojure: 12 | 13 | strategy: 14 | matrix: 15 | java-version: [11, 17, 21] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-java@v4 23 | with: 24 | distribution: temurin 25 | java-version: ${{ matrix.java-version }} 26 | 27 | - name: Print java version 28 | run: java -version 29 | 30 | - name: Install dependencies 31 | run: lein deps 32 | 33 | - name: Run clj tests 34 | run: lein test 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | test-clojure: 10 | strategy: 11 | matrix: 12 | java-version: [11, 17, 21] 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-java@v4 20 | with: 21 | distribution: temurin 22 | java-version: ${{ matrix.java-version }} 23 | 24 | - name: Print java version 25 | run: java -version 26 | 27 | - name: Install dependencies 28 | run: lein deps 29 | 30 | - name: Run clj tests 31 | run: lein test 32 | 33 | release: 34 | name: 'Publish on Clojars' 35 | runs-on: ubuntu-latest 36 | needs: [test-clojure] 37 | steps: 38 | - uses: actions/checkout@v4.2.2 39 | 40 | - name: Install dependencies 41 | run: lein deps 42 | 43 | - name: Publish on Clojars 44 | run: lein deploy publish 45 | env: 46 | CLOJARS_USERNAME: eng-prod-nubank 47 | CLOJARS_PASSWD: ${{ secrets.CLOJARS_DEPLOY_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | profiles.clj 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | /.lein-* 10 | /.nrepl-port 11 | .hgignore 12 | .hg/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 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 | 7 | ## 0.3.0 8 | - Correctly handle binary files in create-blob! and get-blob operations 9 | - Fix reflective accesses in clj-github-mock.impl.jgit 10 | - Remove base64-clj dependency 11 | - Bump dependencies 12 | - org.eclipse.jgit/org.eclipse.jgit from 5.11.0 to 6.10.0 13 | - metosin/reitit-ring from 0.5.13 to 0.7.2 14 | - datascript from 1.1.0 to 1.7.3 15 | 16 | ## 0.2.0 17 | - Bump some libs 18 | 19 | ## 0.1.0 20 | - Initial version implementing most of github database api (https://docs.github.com/en/rest/reference/git) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clj-github-mock 2 | 3 | _current version:_ 4 | 5 | [![Current Version](https://img.shields.io/clojars/v/dev.nubank/clj-github-mock.svg)](https://clojars.org/dev.nubank/clj-github-mock) 6 | 7 | _docs:_ 8 | [Found on cljdoc](https://cljdoc.xyz/d/nubank/clj-github-mock/) 9 | 10 | `clj-github-mock` provides a `ring` like handler that emulates the github api. 11 | Since most clojure http servers are compatible with the `ring` protocol, 12 | you can use the handler with either `ring` itself or `http-kit` or `pedestal` for example. 13 | Most commonly though you will use the handler combined with a library that fakes http requests 14 | like `clj-http-fake` or `http-kit-fake`. That way you can test that your code that interacts 15 | with the github api is working by using `clj-github-mock` behind the scenes. 16 | 17 | **Important**: `clj-github-mock` does not cover the entire github api and should not 18 | be used as the only way to test your github application. Make sure you test your code 19 | against the real github api itself. 20 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | Anybody with write access to this repository can release a new version and deploy it to Clojars. To do this, first make sure your local main is sync'd with main on github: 4 | 5 | ```bash 6 | git switch main 7 | git pull 8 | ``` 9 | 10 | Now run this command: 11 | ``` 12 | ./release.sh 13 | ``` 14 | 15 | The `release.sh` script creates a git tag with the project's current version and pushes it 16 | to github. This will trigger a GithubAction that tests and uploads JAR files to 17 | Clojars. 18 | 19 | ### Credentials 20 | 21 | Credentials are configured as github secrets: `CLOJARS_USERNAME` and 22 | `CLOJARS_PASSWD`. 23 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject dev.nubank/clj-github-mock "0.4.0" 2 | :description "An emulator of the github api" 3 | :url "https://github.com/nubank/clj-github-mock" 4 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5 | :url "https://www.eclipse.org/legal/epl-2.0/"} 6 | 7 | :repositories [["publish" {:url "https://clojars.org/repo" 8 | :username :env/clojars_username 9 | :password :env/clojars_passwd 10 | :sign-releases false}]] 11 | 12 | :plugins [[lein-cljfmt "0.9.2"] 13 | [lein-cloverage "1.2.4"] 14 | [lein-vanity "0.2.0"] 15 | [lein-nsorg "0.3.0"]] 16 | 17 | :dependencies [[org.clojure/clojure "1.12.0"] 18 | ; NOTE: can't upgrade to 7.X because it doesn't support JDK 11 anymore 19 | [org.eclipse.jgit/org.eclipse.jgit "6.10.0.202406032230-r"] 20 | [metosin/reitit-ring "0.7.2"] 21 | [ring/ring-json "0.5.1"] 22 | [ring/ring-mock "0.4.0"] 23 | [datascript "1.7.3"]] 24 | 25 | :profiles {:dev {:plugins [[lein-project-version "0.1.0"]] 26 | :dependencies [[org.clojure/test.check "1.1.1"] 27 | [nubank/matcher-combinators "3.9.1"] 28 | [metosin/malli "0.16.4"] 29 | [lambdaisland/regal "0.1.175"] 30 | [juji/editscript "0.6.4"] 31 | [reifyhealth/specmonstah "2.1.0"] 32 | [medley "1.4.0"]]}} 33 | 34 | :aliases {"coverage" ["cloverage" "-s" "coverage"] 35 | "lint" ["do" ["cljfmt" "check"] ["nsorg"]] 36 | "lint-fix" ["do" ["cljfmt" "fix"] ["nsorg" "--replace"]] 37 | "loc" ["vanity"]}) 38 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | project_version="$(lein project-version | tail -n1)" 4 | 5 | git tag "$project_version" 6 | git push origin "$project_version" 7 | -------------------------------------------------------------------------------- /src/clj_github_mock/core.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.core 2 | (:require [clj-github-mock.handlers.repos :as repos] 3 | [clj-github-mock.impl.database :as database] 4 | [ring.middleware.json :as middleware.json] 5 | [ring.mock.request :as mock])) 6 | 7 | (defn ring-handler 8 | "Creates a ring like handler that emulates the github api. 9 | Receives a map of options that configure the handler. 10 | 11 | Options: 12 | - `:initial-state`: a map containing information about organizations 13 | and repositories that will form the initial state of the emulator. 14 | 15 | Example: 16 | ``` 17 | {:orgs [{:name \"nubank\" :repos [{:name \"some-repo\" :default_branch \"master\"}]}]} 18 | ``` 19 | 20 | `default_branch` is optional and will default to \"main\". 21 | " 22 | [{:keys [initial-state] :as _opts}] 23 | (let [conn (database/create initial-state)] 24 | (-> (repos/handler conn) 25 | (middleware.json/wrap-json-body {:keywords? true}) 26 | (middleware.json/wrap-json-response)))) 27 | 28 | (defn httpkit-fake-handler 29 | "Creates a `ring-handler` that is compatible with `http-kit-fake`. Receives the same 30 | options as `ring-handler." 31 | [opts] 32 | (let [handler (ring-handler opts)] 33 | (fn [_ {:keys [method url body headers] :as req} _] 34 | (handler (merge (reduce 35 | (fn [req [header value]] 36 | (mock/header req header value)) 37 | (-> (mock/request method url) 38 | (mock/body body)) 39 | headers) 40 | (dissoc req :body :headers)))))) 41 | -------------------------------------------------------------------------------- /src/clj_github_mock/handlers/repos.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.handlers.repos 2 | (:require [clj-github-mock.impl.database :as database] 3 | [clj-github-mock.impl.jgit :as jgit] 4 | [clojure.string :as string] 5 | [reitit.ring :as ring] 6 | [ring.middleware.params :as middleware.params])) 7 | 8 | (defn repo-body [org-name {repo-name :repo/name attrs :repo/attrs}] 9 | (merge 10 | {:name repo-name 11 | :full_name (string/join "/" [org-name repo-name])} 12 | attrs)) 13 | 14 | (defn get-repos-handler [{database :database {:keys [org]} :path-params}] 15 | {:status 200 16 | :body (mapv (partial repo-body org) (database/find-repos database org))}) 17 | 18 | (defn post-repos-handler [{state :database 19 | {:keys [org]} :path-params 20 | {:keys [name] :as repo} :body}] 21 | (if name 22 | (do 23 | (database/upsert-repo state org name (dissoc repo :name)) 24 | {:status 201 25 | :body (repo-body org (database/find-repo state org name))}) 26 | {:status 422})) 27 | 28 | (defn get-repo-handler [{repo :repo 29 | {:keys [org]} :path-params}] 30 | {:status 200 31 | :body (repo-body org repo)}) 32 | 33 | (defn patch-repo-handler [{database :database 34 | {:keys [org repo]} :path-params 35 | body :body}] 36 | (database/upsert-repo database org repo body) 37 | {:status 200 38 | :body (repo-body org (database/find-repo database org repo))}) 39 | 40 | (defn post-tree-handler [{{git-repo :repo/jgit} :repo 41 | body :body}] 42 | {:status 201 43 | :body (jgit/create-tree! git-repo body)}) 44 | 45 | (defn get-tree-handler [{{git-repo :repo/jgit} :repo 46 | {:keys [sha]} :path-params}] 47 | (if-let [body (jgit/get-tree git-repo sha)] 48 | {:status 200 49 | :body body} 50 | {:status 404})) 51 | 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 | 62 | (defn post-commit-handler [{{git-repo :repo/jgit} :repo 63 | body :body}] 64 | {:status 201 65 | :body (jgit/create-commit! git-repo body)}) 66 | 67 | (defn get-commit-handler [{{git-repo :repo/jgit} :repo 68 | {:keys [sha]} :path-params}] 69 | (if-let [body (jgit/get-commit git-repo sha)] 70 | {:status 200 71 | :body body} 72 | {:status 404})) 73 | 74 | (defn post-ref-handler [{{git-repo :repo/jgit} :repo 75 | body :body}] 76 | {:status 201 77 | :body (jgit/create-reference! git-repo body)}) 78 | 79 | (defn patch-ref-handler [{{git-repo :repo/jgit} :repo 80 | {:keys [ref]} :path-params 81 | body :body}] 82 | {:status 200 83 | :body (jgit/create-reference! git-repo (merge {:ref (str "refs/" ref)} 84 | body))}) 85 | 86 | (defn get-ref-handler [{{git-repo :repo/jgit} :repo 87 | {:keys [ref]} :path-params}] 88 | (if-let [body (jgit/get-reference git-repo (str "refs/" ref))] 89 | {:status 200 90 | :body body} 91 | {:status 404})) 92 | 93 | (defn delete-ref-handler [{{git-repo :repo/jgit} :repo 94 | {:keys [ref]} :path-params}] 95 | (jgit/delete-reference! git-repo (str "refs/" ref)) 96 | {:status 204}) 97 | 98 | (defn get-branch-handler [{{git-repo :repo/jgit} :repo 99 | {:keys [branch]} :path-params}] 100 | (if-let [branch (jgit/get-branch git-repo branch)] 101 | {:status 200 102 | :body branch} 103 | {:status 404})) 104 | 105 | (defn- sha? [ref] 106 | (and ref 107 | (re-find #"^[A-Fa-f0-9]{40}$" ref))) 108 | 109 | (defn- content-sha [git-repo ref default_branch] 110 | (if (sha? ref) 111 | ref 112 | (let [branch (or ref default_branch)] 113 | (get-in (jgit/get-reference git-repo (str "refs/heads/" branch)) [:object :sha])))) 114 | 115 | (defn get-content-handler [{{git-repo :repo/jgit 116 | {:keys [default_branch]} :repo/attrs} :repo 117 | {:keys [path]} :path-params 118 | {:strs [ref]} :query-params}] 119 | (let [sha (content-sha git-repo ref default_branch) 120 | content (when sha (jgit/get-content git-repo sha path))] 121 | (if content 122 | {:status 200 123 | :body content} 124 | {:status 404}))) 125 | 126 | (defn repo-middleware [handler] 127 | (fn [{database :database {:keys [org repo]} :path-params :as request}] 128 | (let [repo (database/find-repo database org repo)] 129 | (handler (assoc request :repo repo))))) 130 | 131 | (def routes 132 | [["/orgs/:org/repos" {:get get-repos-handler 133 | :post post-repos-handler}] 134 | ["/repos/:org/:repo" {:middleware [repo-middleware]} 135 | ["" {:get get-repo-handler 136 | :patch patch-repo-handler}] 137 | ["/git/trees" {:post post-tree-handler}] 138 | ["/git/trees/:sha" {:get get-tree-handler}] 139 | ["/git/blobs" {:post post-blob-handler}] 140 | ["/git/blobs/:sha" {:get get-blob-handler}] 141 | ["/git/commits" {:post post-commit-handler}] 142 | ["/git/commits/:sha" {:get get-commit-handler}] 143 | ["/git/refs" {:post post-ref-handler}] 144 | ["/git/refs/*ref" {:patch patch-ref-handler 145 | :delete delete-ref-handler}] 146 | ["/git/ref/*ref" {:get get-ref-handler}] 147 | ["/branches/:branch" {:get get-branch-handler}] 148 | ["/contents/*path" {:get get-content-handler}]]]) 149 | 150 | (defn handler [database] 151 | (-> (ring/ring-handler 152 | (ring/router routes) 153 | (ring/create-default-handler)) 154 | (middleware.params/wrap-params) 155 | (database/middleware database))) 156 | -------------------------------------------------------------------------------- /src/clj_github_mock/impl/base64.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.impl.base64 2 | (:require [clojure.string :as str]) 3 | (:import (java.nio.charset StandardCharsets) 4 | (java.util Base64 Base64$Decoder Base64$Encoder))) 5 | 6 | (set! *warn-on-reflection* true) 7 | 8 | (def ^:private ^Base64$Encoder base64-encoder (Base64/getEncoder)) 9 | (def ^:private ^Base64$Decoder base64-decoder (Base64/getDecoder)) 10 | 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 | 27 | (defn encode-bytes->str 28 | "Encodes the given byte array to its Base64 representation." 29 | ^String [^bytes bs] 30 | (let [data (.encode base64-encoder bs)] 31 | (line-wrap (String. data StandardCharsets/UTF_8) 60))) 32 | 33 | (defn encode-str->str 34 | "Encodes the given String to its Base64 representation using UTF-8." 35 | ^String [^String s] 36 | (encode-bytes->str (.getBytes s StandardCharsets/UTF_8))) 37 | 38 | (defn decode-str->bytes 39 | "Decodes the given Base64 String to a byte array." 40 | ^bytes [^String s] 41 | (let [bs (.getBytes (unwrap-lines s) StandardCharsets/UTF_8)] 42 | (.decode base64-decoder bs))) 43 | 44 | (defn decode-str->str 45 | "Decodes the given Base64 String to a new String using UTF-8." 46 | ^String [^String s] 47 | (String. (decode-str->bytes s) StandardCharsets/UTF_8)) 48 | -------------------------------------------------------------------------------- /src/clj_github_mock/impl/database.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.impl.database 2 | (:require [clj-github-mock.impl.jgit :as jgit] 3 | [datascript.core :as d])) 4 | 5 | (def repo-defaults {:default_branch "main"}) 6 | 7 | (defn- repo-datums [org-name repo-name repo-attrs] 8 | [{:repo/id (str (java.util.UUID/randomUUID)) 9 | :repo/name repo-name 10 | :repo/org [:org/name org-name] 11 | :repo/attrs (merge repo-defaults repo-attrs) 12 | :repo/jgit (jgit/empty-repo)}]) 13 | 14 | (defn- org-datums [{org-name :name repos :repos}] 15 | (concat 16 | [{:org/name org-name}] 17 | (mapcat #(repo-datums org-name 18 | (:name %) 19 | (dissoc % :name)) repos))) 20 | 21 | (defn create [{:keys [orgs] :as _initial-state}] 22 | (let [schema {:org/name {:db/unique :db.unique/identity} 23 | :repo/id {:db/unique :db.unique/identity} 24 | :repo/org {:db/valueType :db.type/ref}} 25 | conn (d/create-conn schema)] 26 | (d/transact! conn (mapcat org-datums orgs)) 27 | conn)) 28 | 29 | (defn upsert-repo [database org-name repo-name attrs] 30 | (d/transact! database (repo-datums org-name repo-name attrs))) 31 | 32 | (defn find-repos [database org-name] 33 | (d/q 34 | '[:find [(pull ?r [*]) ...] 35 | :in $ ?org-name 36 | :where 37 | [?r :repo/org ?org] 38 | [?org :org/name ?org-name]] 39 | @database org-name)) 40 | 41 | (defn find-repo [database org-name repo-name] 42 | (d/q '[:find (pull ?r [*]) . 43 | :in $ ?org-name ?repo-name 44 | :where 45 | [?r :repo/name ?repo-name] 46 | [?r :repo/org ?org] 47 | [?org :org/name ?org-name]] @database org-name repo-name)) 48 | 49 | (defn lookup [database eid] 50 | (d/pull @database '[*] eid)) 51 | 52 | (defn middleware [handler conn] 53 | (fn [request] 54 | (handler (assoc request :database conn)))) 55 | -------------------------------------------------------------------------------- /src/clj_github_mock/impl/jgit.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.impl.jgit 2 | (:require [clj-github-mock.impl.base64 :as base64] 3 | [clojure.set :as set] 4 | [clojure.string :as string]) 5 | (:import [org.eclipse.jgit.internal.storage.dfs DfsRepositoryDescription InMemoryRepository] 6 | [org.eclipse.jgit.lib AnyObjectId CommitBuilder Constants FileMode ObjectId 7 | ObjectInserter ObjectReader PersonIdent Repository TreeFormatter] 8 | [org.eclipse.jgit.revwalk RevCommit] 9 | [org.eclipse.jgit.treewalk TreeWalk])) 10 | 11 | (set! *warn-on-reflection* true) 12 | 13 | (defn empty-repo [] 14 | (InMemoryRepository. (DfsRepositoryDescription.))) 15 | 16 | (defn- new-inserter ^ObjectInserter [^Repository repo] 17 | (-> repo (.getObjectDatabase) (.newInserter))) 18 | 19 | (defmacro with-inserter [[inserter repo] & body] 20 | `(let [~inserter (new-inserter ~repo) 21 | result# (do ~@body)] 22 | (.flush ~inserter) 23 | result#)) 24 | 25 | (defn- new-reader [^Repository repo] 26 | (-> repo (.getObjectDatabase) (.newReader))) 27 | 28 | (defn- load-object [^ObjectReader reader ^ObjectId object-id] 29 | (let [object-loader (.open reader object-id)] 30 | (.getBytes object-loader))) 31 | 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"))] 37 | (.insert inserter Constants/OBJ_BLOB bs))) 38 | 39 | (defn create-blob! [repo blob] 40 | (with-inserter [inserter repo] 41 | (let [object-id (insert-blob inserter blob)] 42 | {:sha (ObjectId/toString object-id)}))) 43 | 44 | (defn get-blob [repo sha] 45 | ; https://docs.github.com/en/rest/git/blobs?apiVersion=2022-11-28#get-a-blob 46 | (let [content (load-object (new-reader repo) (ObjectId/fromString sha))] 47 | {:content (base64/encode-bytes->str content) 48 | :encoding "base64"})) 49 | 50 | (def ^:private github-mode->file-mode {"100644" FileMode/REGULAR_FILE 51 | "100755" FileMode/EXECUTABLE_FILE 52 | "040000" FileMode/TREE 53 | "160000" FileMode/GITLINK 54 | "120000" FileMode/SYMLINK}) 55 | 56 | (def ^:private file-mode->github-mode (set/map-invert github-mode->file-mode)) 57 | 58 | (def ^:private file-mode->github-type {FileMode/REGULAR_FILE "blob" 59 | FileMode/EXECUTABLE_FILE "blob" 60 | FileMode/TREE "tree" 61 | FileMode/GITLINK "commit" 62 | FileMode/SYMLINK "blob"}) 63 | 64 | (defn- tree-walk-seq [^TreeWalk tree-walk] 65 | (lazy-seq 66 | (when (.next tree-walk) 67 | (cons {:path (.getPathString tree-walk) 68 | :mode (file-mode->github-mode (.getFileMode tree-walk)) 69 | :type (file-mode->github-type (.getFileMode tree-walk)) 70 | :sha (ObjectId/toString (.getObjectId tree-walk 0))} 71 | (tree-walk-seq tree-walk))))) 72 | 73 | (defn tree-walk [^Repository repo sha] 74 | (doto (TreeWalk. repo) 75 | (.reset (ObjectId/fromString sha)))) 76 | 77 | (defn split-path [path] 78 | (string/split path #"/")) 79 | 80 | (defn path-sha [^Repository repo base_tree ^String path] 81 | (when base_tree 82 | (when-let [tree-walk (TreeWalk/forPath repo path ^"[Lorg.eclipse.jgit.lib.AnyObjectId;" (into-array ObjectId [(ObjectId/fromString base_tree)]))] 83 | (ObjectId/toString (.getObjectId tree-walk 0))))) 84 | 85 | (defn leaf-item? [item] 86 | (not (map? item))) 87 | 88 | (defn tree-items->tree-map [tree-items] 89 | (->> (reduce (fn [acc {:keys [path] :as item}] 90 | (let [path-split (split-path path) 91 | item-path (last path-split)] 92 | (update-in acc path-split conj (assoc item :path item-path)))) 93 | {} tree-items))) 94 | 95 | (defn tree-map->tree-items [tree-map repo base_tree] 96 | (mapcat 97 | (fn [[path item]] 98 | (if-not (leaf-item? item) 99 | (let [tree-sha (path-sha repo base_tree path)] 100 | [{:path path 101 | :type "tree" 102 | :mode "040000" 103 | :base_tree tree-sha 104 | :content (if (map? item) 105 | (tree-map->tree-items item repo tree-sha) 106 | item)}]) 107 | item)) 108 | tree-map)) 109 | 110 | (declare insert-tree) 111 | 112 | (defn content->object-id ^ObjectId [{:keys [type content base_tree] :as blob} repo inserter] 113 | (case type 114 | "blob" (insert-blob inserter blob) 115 | "tree" (insert-tree repo inserter content base_tree))) 116 | 117 | (defn append-tree-item [{:keys [^String path mode sha content] :as item} repo ^TreeFormatter tree-formatter inserter] 118 | (when (or content sha) 119 | (.append tree-formatter 120 | path 121 | ^FileMode (github-mode->file-mode mode) 122 | (if sha 123 | (ObjectId/fromString sha) 124 | (content->object-id item repo inserter))))) 125 | 126 | (defn- merge-trees [base-tree new-tree] 127 | (-> (merge (group-by :path base-tree) 128 | (group-by :path new-tree)) 129 | vals 130 | flatten)) 131 | 132 | (defn insert-tree [repo inserter tree base_tree] 133 | (let [tree-formatter (TreeFormatter.) 134 | tree' (if base_tree 135 | (merge-trees (tree-walk-seq (tree-walk repo base_tree)) 136 | tree) 137 | tree)] 138 | (doseq [tree-item tree'] 139 | (append-tree-item tree-item repo tree-formatter inserter)) 140 | (.insertTo tree-formatter inserter))) 141 | 142 | (defn get-tree [repo sha] 143 | {:sha sha 144 | :tree (into [] (tree-walk-seq (tree-walk repo sha)))}) 145 | 146 | (defn- concat-path [base-path path] 147 | (if base-path (str base-path "/" path) path)) 148 | 149 | (defn- flatten-tree [repo base-sha base-path] 150 | (let [{:keys [tree]} (get-tree repo base-sha)] 151 | (mapcat (fn [{:keys [path type sha] :as tree-item}] 152 | (if (= "tree" type) 153 | (flatten-tree repo sha (concat-path base-path path)) 154 | [(-> (merge tree-item (get-blob repo (:sha tree-item))) 155 | ; NOTE: when reading the flattened tree, contents are always assumed to be a String 156 | ; (needed for backwards compatibility) 157 | (update :content #(if (string/blank? %) % (base64/decode-str->str %))) 158 | (assoc :encoding "utf-8") 159 | (update :path (partial concat-path base-path)) 160 | (dissoc :sha))])) 161 | tree))) 162 | 163 | (defn get-flatten-tree [repo sha] 164 | {:sha sha 165 | :tree (into [] (flatten-tree repo sha nil))}) 166 | 167 | (defn create-tree! [repo {:keys [tree base_tree]}] 168 | (let [final-tree (-> (tree-items->tree-map tree) 169 | (tree-map->tree-items repo base_tree))] 170 | (let [sha (with-inserter [inserter repo] 171 | (ObjectId/toString (insert-tree repo inserter final-tree base_tree)))] 172 | (get-tree repo sha)))) 173 | 174 | ;; only for testing - start 175 | (declare tree-content) 176 | 177 | (defn- tree-item-content [repo {:keys [type sha]}] 178 | (case type 179 | "tree" (tree-content repo sha) 180 | "blob" (get-blob repo sha))) 181 | 182 | (defn tree-content [repo sha] 183 | (let [tree (tree-walk-seq (tree-walk repo sha))] 184 | (->> tree 185 | (map (fn [{:keys [path] :as tree-item}] 186 | [path (tree-item-content repo tree-item)])) 187 | (into {})))) 188 | ;; only for testing - end 189 | 190 | (defn get-commit [repo sha] 191 | (let [bytes (load-object (new-reader repo) (ObjectId/fromString sha)) 192 | commit (RevCommit/parse bytes)] 193 | {:sha sha 194 | :message (.getFullMessage commit) 195 | :tree {:sha (-> commit (.getTree) (.getId) (.getName))} 196 | :parents (mapv #(hash-map :sha (ObjectId/toString (.getId ^RevCommit %))) (.getParents commit))})) 197 | 198 | (defn create-commit! [repo {:keys [tree message parents]}] 199 | (let [commit-id (with-inserter [inserter repo] 200 | (let [commit-builder (doto (CommitBuilder.) 201 | (.setMessage message) 202 | (.setTreeId (ObjectId/fromString tree)) 203 | (.setAuthor (PersonIdent. "me" "me@example.com")) 204 | (.setCommitter (PersonIdent. "me" "me@example.com")) 205 | (.setParentIds ^"[Lorg.eclipse.jgit.lib.ObjectId;" (into-array ObjectId (map #(ObjectId/fromString %) parents))))] 206 | (.insert inserter commit-builder)))] 207 | (get-commit repo (ObjectId/toString commit-id)))) 208 | 209 | (defn get-reference [^Repository repo ^String ref-name] 210 | (when-let [ref (.exactRef repo ref-name)] 211 | {:ref ref-name 212 | :object {:type "commit" 213 | :sha (ObjectId/toString (.getObjectId ref))}})) 214 | 215 | (defn create-reference! [^Repository repo {:keys [^String ref sha]}] 216 | (let [ref-update (.updateRef repo ref)] 217 | (doto ref-update 218 | (.setNewObjectId (ObjectId/fromString sha)) 219 | (.update)) 220 | (get-reference repo ref))) 221 | 222 | (defn delete-reference! [^Repository repo ^String ref] 223 | (.delete 224 | (doto (.updateRef repo ref) (.setForceUpdate true)))) 225 | 226 | (defn get-branch [^Repository repo ^String branch] 227 | (when-let [ref (.findRef repo branch)] 228 | (let [commit (get-commit repo (ObjectId/toString (.getObjectId ref)))] 229 | {:name branch 230 | :commit {:sha (:sha commit) 231 | :commit (dissoc commit :sha)}}))) 232 | 233 | (defn get-content [repo sha path] 234 | ; https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content 235 | (let [reader (new-reader repo) 236 | commit (RevCommit/parse (load-object reader (ObjectId/fromString sha))) 237 | tree-id (-> commit (.getTree) (.getId)) 238 | tree-walk (TreeWalk/forPath ^ObjectReader reader ^String path ^"[Lorg.eclipse.jgit.lib.AnyObjectId;" (into-array AnyObjectId [tree-id])) 239 | object-id (when tree-walk (.getObjectId tree-walk 0))] 240 | (when object-id 241 | (let [content (load-object reader object-id)] 242 | {:type "file" 243 | :path path 244 | :encoding "base64" 245 | :content (base64/encode-bytes->str content)})))) 246 | -------------------------------------------------------------------------------- /test/clj_github_mock/generators.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.generators 2 | (:require [clj-github-mock.handlers.repos :as repos] 3 | [clj-github-mock.impl.database :as database] 4 | [clj-github-mock.impl.jgit :as jgit] 5 | [clojure.test.check.generators :as gen] 6 | [clojure.test.check.random :as random] 7 | [clojure.test.check.rose-tree :as rose] 8 | [clojure.walk :as walk] 9 | [datascript.core :as d] 10 | [lambdaisland.regal.generator :as regal-gen] 11 | [malli.generator :as mg] 12 | [medley.core :refer [assoc-some map-keys map-vals]] 13 | [reifyhealth.specmonstah.core :as sm] 14 | [reifyhealth.specmonstah.spec-gen :as sg])) 15 | 16 | (def object-name 17 | "Generates a name for objects like org names, repo names, branches, tags, etc." 18 | (regal-gen/gen [:+ :word])) 19 | 20 | (defn unique-object-name 21 | "Creates a object-name generator that returns unique names." 22 | [] 23 | (let [names (atom #{})] 24 | (gen/such-that 25 | #(let [result (@names %)] 26 | (swap! names conj %) 27 | (not result)) 28 | object-name))) 29 | 30 | (def blob 31 | "Generates a string that can be used as a blob content." 32 | gen/string-ascii) 33 | 34 | (defn- flatten-map-tree-entry [[obj-name node]] 35 | (if (:type node) 36 | {obj-name node} 37 | (map-keys #(str obj-name "/" %) node))) 38 | 39 | (defn- map-tree->github-tree [map-tree] 40 | (->> (walk/postwalk 41 | (fn [node] 42 | (if (and (map? node) (not (:type node))) 43 | (apply merge (map flatten-map-tree-entry node)) 44 | node)) 45 | map-tree) 46 | (mapv (fn [[path tree-object]] 47 | (assoc tree-object :path path))))) 48 | 49 | (def ^:private modes-frequency {"100644" 95 "100755" 5}) 50 | 51 | (def ^:private github-tree-object 52 | (mg/generator [:map 53 | [:type [:= "blob"]] 54 | [:mode (into [:enum {:gen/gen (gen/frequency (mapv #(vector (val %) (gen/return (key %))) modes-frequency))} (keys modes-frequency)])] 55 | [:content [:string {:gen/gen blob}]]])) 56 | 57 | (def github-tree 58 | "Generates a vector of github tree objects. A github tree object is a map 59 | representing a file with the following fields: 60 | 61 | - `:path`: a string with the path of the object, the path can be nested, 62 | in which case it will be separated by slashes 63 | - `:type`: this generator only creates object of the \"blob\" type 64 | - `:mode`: either `100644` for normal files and `100755` for executable files 65 | note: although the generator can return files with executable mode, the 66 | content of the file is not actualy something that can be executed 67 | - `:content`: a string with the content of the file" 68 | (gen/let [map-tree (gen/recursive-gen 69 | #(gen/map object-name % {:min-elements 1}) 70 | github-tree-object) 71 | root-name (if (:type map-tree) object-name (gen/return nil))] 72 | (if root-name 73 | [(assoc map-tree :path root-name)] 74 | (map-tree->github-tree map-tree)))) 75 | 76 | (defn- update-gen [github-tree] 77 | (gen/let [item (gen/elements github-tree) 78 | new-content (gen/not-empty gen/string)] 79 | (assoc item :content new-content))) 80 | 81 | (defn- delete-gen [github-tree] 82 | (gen/let [item (gen/elements github-tree)] 83 | (-> item 84 | (dissoc :content) 85 | (assoc :sha nil)))) 86 | 87 | ; TODO support creating new files 88 | (defn github-tree-changes 89 | "Creates a generator that given a github tree generates changes in that tree. 90 | The changes are themselves a github tree." 91 | [github-tree] 92 | (if (empty? github-tree) 93 | (gen/return github-tree) 94 | (gen/vector-distinct-by :path (gen/frequency [[9 (update-gen github-tree)] [1 (delete-gen github-tree)]]) {:min-elements 1}))) 95 | 96 | (defn tree 97 | "Creates a generator that given a jgit repository generates a tree in it. 98 | If passed the sha of a tree in the repository, generates a tree that is 99 | derived from the one whose sha was passed. 100 | 101 | Note: the generator in not purely functional since a jgit repository is mutable." 102 | ([repo] 103 | (tree repo nil)) 104 | ([repo base-tree-sha] 105 | (let [{:keys [tree]} (when base-tree-sha 106 | (jgit/get-flatten-tree repo base-tree-sha))] 107 | (gen/let [new-tree (if tree (github-tree-changes tree) github-tree)] 108 | (jgit/create-tree! repo (assoc-some {:tree new-tree} :base_tree base-tree-sha)))))) 109 | 110 | (defn commit 111 | "Creates a generator that given a jgit repository generates a commit in it. 112 | If passed the sha of a commit in the repository, generates a commit that 113 | is derived from the one whose sha was passed. 114 | 115 | Note: the generator is not purely functional since a jgit repository is mutable." 116 | ([repo] 117 | (commit repo nil)) 118 | ([repo parent-commit-sha] 119 | (let [base-tree-sha (when parent-commit-sha (-> (jgit/get-commit repo parent-commit-sha) :tree :sha))] 120 | (gen/let [tree (tree repo base-tree-sha) 121 | message gen/string] 122 | (jgit/create-commit! repo (assoc-some {:message message :tree (:sha tree)} :parents (when parent-commit-sha [parent-commit-sha]))))))) 123 | 124 | (defn- commit-history [repo base-commit num-commits] 125 | (if (= 0 num-commits) 126 | (gen/return base-commit) 127 | (gen/let [next-commit (commit repo (:sha base-commit))] 128 | (commit-history repo next-commit (dec num-commits))))) 129 | 130 | (defn branch 131 | "Creates a generator that given a jgit repository generates in it a branch pointing to a sequence of commits. 132 | The generator can be customized with the following options: 133 | - `:name`: the name of the branch, if not set a random name is generated 134 | - `num-commits`: the number of commits to be generated, if not set a random number of commits is generated 135 | - `base-branch`: the generated branch will be derived from the base-branch, if not set an orphan branch is generated 136 | 137 | Note: the generator is not purely functional since a jgit repository is mutable" 138 | [repo & {:keys [name num-commits base-branch]}] 139 | (gen/let [branch-name (if name (gen/return name) object-name) 140 | num-commits (if num-commits (gen/return num-commits) (gen/fmap inc (gen/scale #(/ % 10) gen/nat))) 141 | last-commit (commit-history repo (when base-branch (-> (jgit/get-branch repo base-branch) :commit)) num-commits)] 142 | (jgit/create-reference! repo {:ref (str "refs/heads/" branch-name) :sha (:sha last-commit)}) 143 | (jgit/get-branch repo branch-name))) 144 | 145 | (defn random-file 146 | "Creates a generator that given a jgit repository and a branch name randomly selects a file contained in that branch. 147 | The file is returned as a github tree object. 148 | 149 | Note: the generator is not purely functional since a jgit repository is mutable" 150 | [repo branch-name] 151 | (let [branch (jgit/get-branch repo branch-name) 152 | commit (jgit/get-commit repo (-> branch :commit :sha)) 153 | {:keys [tree]} (jgit/get-flatten-tree repo (-> commit :tree :sha))] 154 | (gen/elements tree))) 155 | 156 | (defn- schema [] 157 | {:org {:prefix :org 158 | :malli-schema [:map 159 | [:org/name [:string {:gen/gen (unique-object-name)}]]]} 160 | :repo {:prefix :repo 161 | :malli-schema [:map 162 | [:repo/id :uuid] 163 | [:repo/name [:string {:gen/gen (unique-object-name)}]] 164 | [:repo/attrs [:map 165 | [:default_branch [:= "main"]]]] 166 | [:repo/jgit [:any {:gen/fmap (fn [_] (jgit/empty-repo))}]]] 167 | :relations {:repo/org [:org :org/name]}}}) 168 | 169 | (defn- malli-create-gen 170 | [ent-db] 171 | (update ent-db :schema #(map-vals (fn [{:keys [malli-schema] :as ent-spec}] 172 | (assoc ent-spec :malli-gen (mg/generator malli-schema))) %))) 173 | 174 | (defn- malli-gen-ent-val 175 | [{{:keys [rnd-state size]} :gen-options :as ent-db} {:keys [ent-name]}] 176 | (let [{:keys [malli-gen]} (sm/ent-schema ent-db ent-name) 177 | [rnd1 rnd2] (random/split @rnd-state)] 178 | (reset! rnd-state rnd2) 179 | (rose/root (gen/call-gen malli-gen rnd1 size)))) 180 | 181 | (defn- foreign-key-ent [[_ foreign-key-attr :as path] foreign-key-val] 182 | (cond 183 | ; TODO: use constraints to detect if it is a multi relationship 184 | (vector? foreign-key-val) (set (map (partial foreign-key-ent path) foreign-key-val)) 185 | :else {foreign-key-attr foreign-key-val})) 186 | 187 | (defn- assoc-ent-at-foreign-keys [db {:keys [ent-type spec-gen]}] 188 | (reduce 189 | (fn [acc [attr relation-path]] 190 | (update acc attr (partial foreign-key-ent relation-path))) 191 | spec-gen 192 | (-> db :schema ent-type :relations))) 193 | 194 | (def ^:private malli-gen [malli-gen-ent-val 195 | sg/spec-gen-merge-overwrites 196 | sg/spec-gen-assoc-relations]) 197 | 198 | (defn- ent-db-malli-gen 199 | [ent-db query] 200 | (-> (malli-create-gen ent-db) 201 | (sm/add-ents query) 202 | (sm/visit-ents-once :spec-gen malli-gen))) 203 | 204 | (defn- insert-datascript [database ent-db {:keys [spec-gen] :as ent-attrs}] 205 | (let [datoms (assoc-ent-at-foreign-keys ent-db ent-attrs)] 206 | (d/transact! database [datoms]) 207 | spec-gen)) 208 | 209 | (defn- insert [database ent-db ent-attrs] 210 | (insert-datascript database ent-db ent-attrs)) 211 | 212 | (defn- ent-data [ent-db ent] 213 | (:inserted-data (sm/ent-attrs ent-db ent))) 214 | 215 | (defn- ent-attrs-map [ent-db] 216 | (let [ents (sm/ents ent-db)] 217 | (zipmap ents 218 | (map (partial ent-data ent-db) ents)))) 219 | 220 | (defn- ents-attrs-map [ent-db] 221 | (let [ents-by-type (sm/ents-by-type ent-db)] 222 | (zipmap (keys ents-by-type) 223 | (map #(map (partial ent-data ent-db) %) 224 | (vals ents-by-type))))) 225 | 226 | (defn database 227 | "Creates a generator that given a specmonstah query generates a `clj-github-mock.database`. 228 | The `schema` var in this namespace contains the specmonstah schema used by this generator. 229 | The generator returns a map from the ent names to ent attributes plus the following attributes: 230 | - `:handler`: a `clj-github-mock.repos/handler` pointing to the generated database 231 | - `:database`: the datascript connection to the database 232 | - `:ent-db`: the ent-db generated by specmonstah 233 | - `:ents`: a map from ent type to a collection of the ents generated for that type" 234 | [query] 235 | (gen/->Generator 236 | (fn [rnd size] 237 | (let [database (database/create {}) 238 | ent-db (-> (ent-db-malli-gen {:schema (schema) 239 | :gen-options {:rnd-state (atom rnd) 240 | :size size}} 241 | query) 242 | (sm/visit-ents-once :inserted-data (partial insert database)))] 243 | (rose/pure 244 | (merge 245 | {:handler (repos/handler database) 246 | :database database 247 | :ent-db ent-db 248 | :ents (ents-attrs-map ent-db)} 249 | (ent-attrs-map ent-db))))))) 250 | -------------------------------------------------------------------------------- /test/clj_github_mock/handlers/repos_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.handlers.repos-test 2 | (:require [clj-github-mock.generators :as mock-gen] 3 | [clj-github-mock.impl.base64 :as base64] 4 | [clj-github-mock.impl.database :as database] 5 | [clj-github-mock.impl.jgit :as jgit] 6 | [clojure.data :as data] 7 | [clojure.string :as string] 8 | [clojure.test.check.clojure-test :refer [defspec]] 9 | [clojure.test.check.generators :as gen] 10 | [clojure.test.check.properties :as prop] 11 | [malli.core :as m] 12 | [matcher-combinators.standalone :refer [match?]] 13 | [matcher-combinators.test] 14 | [ring.mock.request :as mock]) 15 | (:import (java.util Arrays))) 16 | 17 | (defn org-repos-path [org-name] 18 | (str "/orgs/" org-name "/repos")) 19 | 20 | (def list-org-repos-response-schema 21 | [:map 22 | [:status [:= 200]] 23 | [:body [:vector 24 | [:map 25 | [:name :string] 26 | [:full_name :string] 27 | [:default_branch :string]]]]]) 28 | 29 | (defn list-org-repos-request [org-name] 30 | (mock/request :get (org-repos-path org-name))) 31 | 32 | (defspec list-org-repos-respects-response-schema 33 | (prop/for-all 34 | [{:keys [handler ent-db org0]} (mock-gen/database {:repo [[3]]})] 35 | (m/validate list-org-repos-response-schema 36 | (handler (list-org-repos-request (:org/name org0)))))) 37 | 38 | (defspec list-org-repos-return-all-repos 39 | (prop/for-all 40 | [{:keys [handler ent-db ents]} (mock-gen/database {:org [[:org1] 41 | [:org2] 42 | [:org3]] 43 | :repo [[2 {:refs {:repo/org :org1}}] 44 | [2 {:refs {:repo/org :org2}}] 45 | [2 {:refs {:repo/org :org3}}]]})] 46 | (match? (set (map :repo/name (:repo ents))) 47 | (set (map :name (mapcat #(:body (handler (list-org-repos-request (:org/name %)))) (:org ents))))))) 48 | 49 | (def create-org-repo-response-schema 50 | [:map 51 | [:status [:= 201]] 52 | [:body [:map 53 | [:name :string] 54 | [:full_name :string] 55 | [:default_branch :string]]]]) 56 | 57 | (defn create-org-repo-request [org body] 58 | (-> (mock/request :post (org-repos-path org)) 59 | (assoc :body body))) 60 | 61 | (defspec create-org-repo-respects-response-schema 62 | (prop/for-all 63 | [{:keys [handler org0]} (mock-gen/database {:org [[1]]}) 64 | repo-name mock-gen/object-name] 65 | (m/validate 66 | create-org-repo-response-schema 67 | (handler (create-org-repo-request (:org/name org0) {:name repo-name}))))) 68 | 69 | (defspec create-org-repo-requires-a-name 70 | (prop/for-all 71 | [{:keys [handler org0]} (mock-gen/database {:org [[1]]})] 72 | (match? {:status 422} 73 | (handler (create-org-repo-request (:org/name org0) {}))))) 74 | 75 | (defspec create-org-repo-adds-repo-to-the-org 76 | (prop/for-all 77 | [{:keys [handler database org0]} (mock-gen/database {:org [[1]]}) 78 | repo-name mock-gen/object-name] 79 | (handler (create-org-repo-request (:org/name org0) {:name repo-name})) 80 | (database/find-repo database (:org/name org0) repo-name))) 81 | 82 | (def get-org-repo-response-schema 83 | [:map 84 | [:status [:= 200]] 85 | [:body [:map 86 | [:name :string] 87 | [:full_name :string] 88 | [:default_branch :string]]]]) 89 | 90 | (defn org-repo-path [org-name repo-name] 91 | (string/join "/" ["/repos" org-name repo-name])) 92 | 93 | (defn get-org-repo-request [org-name repo-name] 94 | (mock/request :get (org-repo-path org-name repo-name))) 95 | 96 | (defspec get-org-repo-respects-response-schema 97 | (prop/for-all 98 | [{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]})] 99 | (m/validate 100 | get-org-repo-response-schema 101 | (handler (get-org-repo-request (:org/name org0) (:repo/name repo0)))))) 102 | 103 | (defn update-org-repo-request [org-name repo-name body] 104 | (-> (mock/request :patch (org-repo-path org-name repo-name)) 105 | (assoc :body body))) 106 | 107 | (defspec update-org-repo-respects-response-schema 108 | (prop/for-all 109 | [{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]})] 110 | (m/validate 111 | get-org-repo-response-schema 112 | (handler (update-org-repo-request (:org/name org0) (:repo/name repo0) {:random-attr "random-value"}))))) 113 | 114 | (defspec update-org-repo-only-updates-set-fields 115 | (prop/for-all 116 | [{:keys [handler org0 repo0]} (mock-gen/database {:repo [[1]]})] 117 | (let [repo-before (:body (handler (get-org-repo-request (:org/name org0) (:repo/name repo0)))) 118 | _ (handler (update-org-repo-request (:org/name org0) (:repo/name repo0) {:visibility "private"})) 119 | repo-after (:body (handler (get-org-repo-request (:org/name org0) (:repo/name repo0))))] 120 | (match? [nil {:visibility "private"} any?] 121 | (data/diff repo-before repo-after))))) 122 | 123 | (defn trees-path [org repo] 124 | (str "/repos/" org "/" repo "/git/trees")) 125 | 126 | (defn tree-sha-path [org repo tree-sha] 127 | (str (trees-path org repo) "/" tree-sha)) 128 | 129 | (defn create-tree-request [org repo body] 130 | (-> 131 | (mock/request :post (trees-path org repo)) 132 | (assoc :body body))) 133 | 134 | (defn get-tree-request [org repo tree-sha] 135 | (mock/request :get (tree-sha-path org repo tree-sha))) 136 | 137 | (defspec create-tree-adds-tree-to-repo 138 | (prop/for-all 139 | [{:keys [handler database org0 repo0]} (mock-gen/database {:repo [[1]]}) 140 | tree mock-gen/github-tree] 141 | (let [{{:keys [sha]} :body} (handler (create-tree-request (:org/name org0) (:repo/name repo0) {:tree tree}))] 142 | (-> repo0 :repo/jgit (jgit/get-tree sha))))) 143 | 144 | (def get-tree-response-schema 145 | [:map 146 | [:status [:= 200]] 147 | [:body [:map 148 | [:sha :string] 149 | [:tree [:vector 150 | [:map 151 | [:path :string] 152 | [:mode :string] 153 | [:type :string] 154 | [:sha :string]]]]]]]) 155 | 156 | (defspec get-tree-respects-response-schema 157 | (prop/for-all 158 | [{:keys [handler org0 repo0 tree]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 159 | tree (mock-gen/tree (:repo/jgit repo0))] 160 | (assoc database :tree tree))] 161 | (m/validate get-tree-response-schema 162 | (handler (get-tree-request (:org/name org0) (:repo/name repo0) (:sha tree)))))) 163 | 164 | (defn commits-path [org repo] 165 | (str "/repos/" org "/" repo "/git/commits")) 166 | 167 | (defn commit-sha-path [org repo sha] 168 | (str (commits-path org repo) "/" sha)) 169 | 170 | (defn create-commit-request [org repo body] 171 | (-> (mock/request :post (commits-path org repo)) 172 | (assoc :body body))) 173 | 174 | (defn get-commit-request [org repo sha] 175 | (mock/request :get (commit-sha-path org repo sha))) 176 | 177 | (defspec create-commit-adds-commit-to-repo 178 | (prop/for-all 179 | [{:keys [handler org0 repo0 tree]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 180 | tree (mock-gen/tree (:repo/jgit repo0))] 181 | (assoc database :tree tree)) 182 | message gen/string] 183 | (let [{{:keys [sha]} :body} (handler (create-commit-request (:org/name org0) 184 | (:repo/name repo0) 185 | {:message message 186 | :tree (:sha tree)}))] 187 | (-> repo0 :repo/jgit (jgit/get-commit sha))))) 188 | 189 | (def get-commit-response-schema 190 | [:map 191 | [:status [:= 200]] 192 | [:body [:map 193 | [:sha :string] 194 | [:tree [:map 195 | [:sha :string]]] 196 | [:message :string]]]]) 197 | 198 | (defspec get-commit-respects-response-schema 199 | (prop/for-all 200 | [{:keys [handler org0 repo0 commit]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 201 | commit (mock-gen/commit (:repo/jgit repo0))] 202 | (assoc database :commit commit))] 203 | (m/validate get-commit-response-schema 204 | (handler (get-commit-request (:org/name org0) (:repo/name repo0) (:sha commit)))))) 205 | 206 | (defn ref-path [org repo ref] 207 | (str "/repos/" org "/" repo "/git/ref/" ref)) 208 | 209 | (defn refs-path [org repo] 210 | (str "/repos/" org "/" repo "/git/refs")) 211 | 212 | (defn refs-ref-path [org repo ref] 213 | (str (refs-path org repo) "/" ref)) 214 | 215 | (defn create-ref-request [org repo body] 216 | (-> (mock/request :post (refs-path org repo)) 217 | (assoc :body body))) 218 | 219 | (defn update-ref-request [org repo ref body] 220 | (-> (mock/request :patch (refs-ref-path org repo ref)) 221 | (assoc :body body))) 222 | 223 | (defn delete-ref-request [org repo ref] 224 | (mock/request :delete (refs-ref-path org repo ref))) 225 | 226 | (defspec create-ref-adds-ref-to-repo 227 | (prop/for-all 228 | [{:keys [handler org0 repo0 commit]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 229 | commit (mock-gen/commit (:repo/jgit repo0))] 230 | (assoc database :commit commit)) 231 | ref (gen/fmap #(str "refs/heads/" %) mock-gen/object-name)] 232 | (handler (create-ref-request (:org/name org0) (:repo/name repo0) {:ref ref 233 | :sha (:sha commit)})) 234 | (-> repo0 :repo/jgit (jgit/get-reference ref)))) 235 | 236 | (defspec update-ref-updates-the-ref 237 | (prop/for-all 238 | [{:keys [handler org0 repo0 branch commit]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 239 | branch (mock-gen/branch (:repo/jgit repo0)) 240 | commit (mock-gen/commit (:repo/jgit repo0) (-> branch :commit :sha))] 241 | (assoc database :branch branch :commit commit))] 242 | (handler (update-ref-request (:org/name org0) (:repo/name repo0) (str "heads/" (:name branch)) {:sha (:sha commit)})) 243 | (= (:sha commit) (-> repo0 :repo/jgit (jgit/get-reference (str "refs/heads/" (:name branch))) :object :sha)))) 244 | 245 | (defspec delete-ref-removes-ref-from-repo 246 | (prop/for-all 247 | [{:keys [handler org0 repo0 branch]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 248 | branch (mock-gen/branch (:repo/jgit repo0))] 249 | (assoc database :branch branch))] 250 | (handler (delete-ref-request (:org/name org0) (:repo/name repo0) (str "heads/" (:name branch)))) 251 | (nil? (-> repo0 :repo/jgit (jgit/get-reference (str "refs/heads/" (:name branch))))))) 252 | 253 | (defn branch-path [org repo branch] 254 | (str "/repos/" org "/" repo "/branches/" branch)) 255 | 256 | (defn get-branch-request [org repo branch] 257 | (mock/request :get (branch-path org repo branch))) 258 | 259 | (defspec get-branch-returns-branch-info 260 | (prop/for-all 261 | [{:keys [handler org0 repo0 branch]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 262 | branch (mock-gen/branch (:repo/jgit repo0))] 263 | (assoc database :branch branch))] 264 | (= {:status 200 265 | :body branch} 266 | (handler (get-branch-request (:org/name org0) (:repo/name repo0) (:name branch)))))) 267 | 268 | (defn contents-path [org repo path] 269 | (str "/repos/" org "/" repo "/contents/" path)) 270 | 271 | (defn get-content-request 272 | ([org repo path] 273 | (mock/request :get (contents-path org repo path))) 274 | ([org repo path ref] 275 | (mock/request :get (contents-path org repo path) {"ref" ref}))) 276 | 277 | (defspec get-content-returns-content 278 | (prop/for-all 279 | [{:keys [handler org0 repo0 branch file]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 280 | branch (mock-gen/branch (:repo/jgit repo0) :num-commits 1) 281 | file (mock-gen/random-file (:repo/jgit repo0) (:name branch))] 282 | (assoc database :branch branch :file file))] 283 | (= {:status 200 284 | :body {:type "file" 285 | :path (:path file) 286 | :encoding "base64" 287 | :content (base64/encode-str->str (:content file))}} 288 | (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (-> branch :commit :sha)))))) 289 | 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 | 334 | (defspec get-content-supports-refs 335 | (prop/for-all 336 | [{:keys [handler org0 repo0 file branch]} (gen/let [{:keys [repo0] :as database} (mock-gen/database {:repo [[1]]}) 337 | branch (mock-gen/branch (:repo/jgit repo0) :num-commits 1) 338 | file (mock-gen/random-file (:repo/jgit repo0) (:name branch))] 339 | (assoc database :branch branch :file file))] 340 | (= {:status 200 341 | :body {:type "file" 342 | :path (:path file) 343 | :encoding "base64" 344 | :content (base64/encode-str->str (:content file))}} 345 | (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file) (:name branch)))))) 346 | 347 | (defspec get-content-supports-default-branch 348 | (prop/for-all 349 | [{:keys [handler org0 repo0 file]} (gen/let [branch-name mock-gen/object-name 350 | {:keys [repo0] :as database} (mock-gen/database {:repo [[1 {:spec-gen {:repo/attrs {:default_branch branch-name}}}]]}) 351 | _ (mock-gen/branch (:repo/jgit repo0) :name branch-name :num-commits 1) 352 | file (mock-gen/random-file (:repo/jgit repo0) branch-name)] 353 | (assoc database :file file))] 354 | (= {:status 200 355 | :body {:type "file" 356 | :path (:path file) 357 | :encoding "base64" 358 | :content (base64/encode-str->str (:content file))}} 359 | (handler (get-content-request (:org/name org0) (:repo/name repo0) (:path file)))))) 360 | -------------------------------------------------------------------------------- /test/clj_github_mock/impl/base64_test.clj: -------------------------------------------------------------------------------- 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))))) 47 | -------------------------------------------------------------------------------- /test/clj_github_mock/impl/jgit_test.clj: -------------------------------------------------------------------------------- 1 | (ns clj-github-mock.impl.jgit-test 2 | (:require [clj-github-mock.generators :as mock-gen] 3 | [clj-github-mock.impl.base64 :as base64] 4 | [clj-github-mock.impl.jgit :as sut] 5 | [clojure.test.check.clojure-test :refer [defspec]] 6 | [clojure.test.check.generators :as gen] 7 | [clojure.test.check.properties :as prop] 8 | [editscript.core :as editscript] 9 | [matcher-combinators.matchers :as matchers] 10 | [matcher-combinators.standalone :refer [match?]]) 11 | (:import (java.util Arrays))) 12 | 13 | (defn decode-base64 [content] 14 | (if (empty? content) 15 | content 16 | (base64/decode-str->str content))) 17 | 18 | (defspec string-blob-is-added-to-repo 19 | (prop/for-all 20 | [content gen/string] 21 | (let [repo (sut/empty-repo) 22 | {:keys [sha]} (sut/create-blob! repo {:content content})] 23 | (= content 24 | (decode-base64 (:content (sut/get-blob repo sha))))))) 25 | 26 | (defspec binary-blob-is-added-to-repo 27 | (prop/for-all 28 | [^bytes content gen/bytes] 29 | (let [repo (sut/empty-repo) 30 | {:keys [sha]} (sut/create-blob! repo {:content (base64/encode-bytes->str content) 31 | :encoding "base64"})] 32 | (Arrays/equals content 33 | (base64/decode-str->bytes (:content (sut/get-blob repo sha))))))) 34 | 35 | (defspec tree-is-added-to-repo 36 | (prop/for-all 37 | [tree mock-gen/github-tree] 38 | (let [repo (sut/empty-repo) 39 | {:keys [sha]} (sut/create-tree! repo {:tree tree})] 40 | (match? {:sha sha 41 | :tree (matchers/in-any-order tree)} 42 | (sut/get-flatten-tree repo sha))))) 43 | 44 | (def github-tree+changes-gen 45 | (gen/let [tree mock-gen/github-tree 46 | changes (mock-gen/github-tree-changes tree)] 47 | {:tree tree :changes changes})) 48 | 49 | (defn changes->edits [changes] 50 | (->> changes 51 | (mapv (fn [{:keys [path content]}] 52 | (if content 53 | [(into (sut/split-path path) [:content]) :r (base64/encode-str->str content)] 54 | [(sut/split-path path) :-]))) 55 | (editscript/edits->script))) 56 | 57 | (defspec create-tree-preserves-base-tree 58 | (prop/for-all 59 | [{:keys [tree changes]} github-tree+changes-gen] 60 | (let [repo (sut/empty-repo) 61 | {base_tree :sha} (sut/create-tree! repo {:tree tree}) 62 | content-before (sut/tree-content repo base_tree) 63 | {sha :sha} (sut/create-tree! repo {:base_tree base_tree :tree changes}) 64 | content-after (sut/tree-content repo sha)] 65 | (= content-after 66 | (editscript/patch content-before (changes->edits changes)))))) 67 | 68 | (defspec commit-is-added-to-repo 69 | (prop/for-all 70 | [tree mock-gen/github-tree 71 | message gen/string] 72 | (let [repo (sut/empty-repo) 73 | {tree-sha :sha} (sut/create-tree! repo {:tree tree}) 74 | {:keys [sha]} (sut/create-commit! repo {:tree tree-sha :message message :parents []})] 75 | (match? {:sha sha 76 | :message message} 77 | (sut/get-commit repo sha))))) 78 | 79 | (defspec create-commit-sets-parent 80 | (prop/for-all 81 | [{:keys [tree changes]} github-tree+changes-gen 82 | message gen/string] 83 | (let [repo (sut/empty-repo) 84 | {parent-tree-sha :sha} (sut/create-tree! repo {:tree tree}) 85 | {parent-sha :sha} (sut/create-commit! repo {:tree parent-tree-sha :message message :parents []}) 86 | {tree-sha :sha} (sut/create-tree! repo {:tree changes :base_tree parent-tree-sha}) 87 | {:keys [sha]} (sut/create-commit! repo {:tree tree-sha :message message :parents [parent-sha]})] 88 | (= (sut/get-commit repo parent-sha) 89 | (sut/get-commit repo (get-in (sut/get-commit repo sha) [:parents 0 :sha])))))) 90 | 91 | (defspec reference-is-added-to-repo 92 | (prop/for-all 93 | [ref (gen/fmap #(str "refs/heads/" %) mock-gen/object-name)] 94 | (let [repo (sut/empty-repo) 95 | {tree-sha :sha} (sut/create-tree! repo {:tree []}) 96 | {sha :sha} (sut/create-commit! repo {:tree tree-sha :message "test"})] 97 | (sut/create-reference! repo {:ref ref :sha sha}) 98 | (= {:object {:sha sha 99 | :type "commit"} 100 | :ref ref} 101 | (sut/get-reference repo ref))))) 102 | 103 | (defspec reference-can-be-deleted 104 | (prop/for-all 105 | [ref (gen/fmap #(str "refs/heads/" %) mock-gen/object-name)] 106 | (let [repo (sut/empty-repo) 107 | {tree-sha :sha} (sut/create-tree! repo {:tree [{:path "a" :mode "100644" :type "blob" :content "a"}]}) 108 | {sha :sha} (sut/create-commit! repo {:tree tree-sha :message "test"})] 109 | (sut/create-reference! repo {:ref ref :sha sha}) 110 | (sut/delete-reference! repo ref) 111 | (nil? (sut/get-reference repo ref))))) 112 | 113 | (defspec can-get-branch-info 114 | (prop/for-all 115 | [branch mock-gen/object-name] 116 | (let [ref (str "refs/heads/" branch) 117 | repo (sut/empty-repo) 118 | {tree-sha :sha} (sut/create-tree! repo {:tree []}) 119 | {sha :sha} (sut/create-commit! repo {:tree tree-sha :message "test"})] 120 | (sut/create-reference! repo {:ref ref :sha sha}) 121 | (= {:name branch 122 | :commit {:sha sha 123 | :commit {:message "test" 124 | :parents [] 125 | :tree {:sha tree-sha}}}} 126 | (sut/get-branch repo branch))))) 127 | 128 | (defspec can-get-content 129 | (prop/for-all 130 | [tree mock-gen/github-tree] 131 | (let [repo (sut/empty-repo) 132 | {tree-sha :sha} (sut/create-tree! repo {:tree tree}) 133 | {:keys [sha]} (sut/create-commit! repo {:tree tree-sha :message "test" :parents []})] 134 | (every? #(= {:type "file" 135 | :path (:path %) 136 | :encoding "base64" 137 | :content (base64/encode-str->str (:content %))} 138 | (sut/get-content repo sha (:path %))) 139 | tree)))) 140 | -------------------------------------------------------------------------------- /test/github-mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nubank/clj-github-mock/b08eae5da4ecbd855e5f4a615be3697bc9a55735/test/github-mark.png -------------------------------------------------------------------------------- /test/github-png-base64: -------------------------------------------------------------------------------- 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= --------------------------------------------------------------------------------