├── .env_sample ├── .formatter.exs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── config ├── config.exs └── test.exs ├── coveralls.json ├── lib ├── git_mock.ex ├── gogs.ex ├── helpers.ex ├── http.ex └── httpoison_mock.ex ├── mix.exs ├── mix.lock └── test ├── git_mock_test.exs ├── gogs_test.exs ├── gogshelpers_test.exs ├── gogshttp_test.exs ├── httpoison_mock_test.exs └── test_helper.exs /.env_sample: -------------------------------------------------------------------------------- 1 | # URL of your Gogs instance without protocol or trailing forward slash: 2 | export GOGS_URL=gogs-server.fly.dev 3 | 4 | # The TCP port defined in your app.ini for connecting with Gogs via SSH: 5 | export GOGS_SSH_PORT=10022 6 | 7 | # Get yours from: $GOGS_URL/user/settings/applications 8 | export GOGS_ACCESS_TOKEN=d6fca75c63daa014c187 9 | 10 | # The absolute path to the SSH *Private* Key 11 | export GOGS_SSH_PRIVATE_KEY_PATH=~/.ssh/id_rsa 12 | 13 | # Optionally set the path where you want to clone git repos: 14 | export GIT_TEMP_DIR_PATH=tmp -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: mix 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "17:00" 8 | timezone: Europe/London 9 | ignore: 10 | # ignore all patch updates in dev dependencies ref: github.com/dwyl/technology-stack/issues/126 [alphabetical list] 11 | - dependency-name: "credo" 12 | update-types: ["version-update:semver-patch"] 13 | - dependency-name: "dialyxir" 14 | update-types: ["version-update:semver-patch"] 15 | - dependency-name: "excoveralls" 16 | update-types: ["version-update:semver-patch"] 17 | - dependency-name: "ex_doc" 18 | update-types: ["version-update:semver-patch"] 19 | - dependency-name: "esbuild" 20 | update-types: ["version-update:semver-patch"] 21 | - dependency-name: "floki" 22 | update-types: ["version-update:semver-patch"] 23 | - dependency-name: "gettext" 24 | update-types: ["version-update:semver-patch"] 25 | - dependency-name: "mock" 26 | update-types: ["version-update:semver-patch"] 27 | - dependency-name: "phoenix_live_dashboard" 28 | update-types: ["version-update:semver-patch"] 29 | - dependency-name: "phoenix_live_reload" 30 | update-types: ["version-update:semver-patch"] 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | # Need this for the nested git submodule: 16 | - name: include test-repo stub Git submodule in base repo 17 | run: git submodule update --init 18 | - name: Setup git user for Git ops 19 | run: | 20 | git config --global user.name "Al Ex" 21 | git config --global user.email "c@t.co" 22 | - name: Set up Elixir 23 | uses: erlef/setup-beam@v1 24 | with: 25 | elixir-version: '1.14.2' # Define the elixir version [required] 26 | otp-version: '25.1.2' # Define the OTP version [required] 27 | - name: Restore dependencies cache 28 | uses: actions/cache@v2 29 | with: 30 | path: deps 31 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 32 | restore-keys: ${{ runner.os }}-mix- 33 | - name: Install dependencies 34 | run: mix deps.get 35 | - name: Run Tests 36 | run: mix coveralls.json 37 | env: 38 | MIX_ENV: test 39 | GOGS_SSH_PORT: 10022 40 | GOGS_URL: gogs-server.fly.dev 41 | GOGS_ACCESS_TOKEN: ${{ secrets.GOGS_ACCESS_TOKEN }} 42 | GIT_TEMP_DIR_PATH: $GITHUB_WORKSPACE 43 | - name: Upload coverage to Codecov 44 | uses: codecov/codecov-action@v1 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | gogs-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # Don't accidentally commit real environment variables 29 | .env 30 | 31 | /public_repo 32 | /public_repo/ 33 | /public_repo/* -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test-repo"] 2 | path = test-repo 3 | url = https://gogs-server.fly.dev/nelsonic/public-repo.git -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | gogs elixir interface 4 | 5 | Interface with a **`Gogs`** instance from **`Elixir`**. 6 | 7 | [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/dwyl/gogs/Elixir%20CI?label=build&style=flat-square)](https://github.com/dwyl/gogs/actions/workflows/ci.yml) 8 | [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/gogs/main.svg?style=flat-square)](https://codecov.io/github/dwyl/gogs?branch=main) 9 | [![Hex.pm](https://img.shields.io/hexpm/v/gogs?color=brightgreen&style=flat-square)](https://hex.pm/packages/gogs) 10 | [![Libraries.io dependency status](https://img.shields.io/librariesio/release/hex/gogs?logoColor=brightgreen&style=flat-square)](https://libraries.io/hex/gogs) 11 | [![docs](https://img.shields.io/badge/docs-maintained-brightgreen?style=flat-square)](https://hexdocs.pm/gogs/api-reference.html) 12 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/gogs/issues) 13 | [![HitCount](https://hits.dwyl.com/dwyl/gogs.svg)](https://hits.dwyl.com/dwyl/gogs) 14 | 17 | 18 |
19 | 20 | # _Why?_ 💡 21 | 22 | We needed an _easy_ way to interact 23 | with our **`Gogs`** (GitHub Backup) **Server** 24 | from our **`Elixir/Phoenix`** App. 25 | This package is that interface. 26 | 27 | > **Note**: We were _briefly_ tempted 28 | > to write this code _inside_ the Phoenix App 29 | > that uses it, 30 | > however we quickly realized 31 | > that having it ***separate*** was better 32 | > for ***testability/maintainability***. 33 | > Having a _separate_ module enforces a 34 | > [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) 35 | > with a strong "API contract". 36 | > This way we know this package is well-tested, 37 | > documented and maintained. 38 | > And can be used and _extended independently_ 39 | > of any `Elixir/Phoenix` app. 40 | > The `Elixir/Phoenix` app can treat `gogs` 41 | > as a logically separate/independent entity 42 | > with a clear interface. 43 | 44 | # _What_? 📦 45 | 46 | A library for interacting with `gogs` (`git`) 47 | from `Elixir` apps.
48 | 49 | Hopefully this diagram explains 50 | how we use the package: 51 | 52 |
53 | 54 | ![Phoenix-Gogs-Infra-dagram](https://user-images.githubusercontent.com/194400/167098379-e06ee8ae-d652-4464-83d7-e209d442e9e2.png) 55 | 56 |
57 | 58 | For the complete list of functions, 59 | please see the docs: https://hexdocs.pm/gogs 📚 60 | 61 | # Who? 👤 62 | 63 | This library is used by our (`Phoenix`) GitHub Backup App.
64 | If you find it helpful for your project, 65 | please ⭐ on GitHub: 66 | [github.com/dwyl/gogs](https://github.com/dwyl/gogs) 67 | 68 | 69 | ## _How_? 💻 70 | 71 | There are a couple of steps to get this working in your project. 72 | It should only take **`2 mins`** if you already have your 73 | **`Gogs` Server** _deployed_ (_or access to an existing instance_). 74 | 75 | 76 | > If you want to read a **step-by-step complete beginner's guide** 77 | > to getting **`gogs`** working in a **`Phoenix`** App, 78 | > please see: 79 | > [github.com/dwyl/**gogs-demo**](https://github.com/dwyl/gogs-demo) 80 | 81 | 82 |
83 | 84 | ## Install ⬇️ 85 | 86 | Install the package from [hex.pm](https://hex.pm/docs/publish), 87 | by adding `gogs` to the list of dependencies in your `mix.exs` file: 88 | 89 | ```elixir 90 | def deps do 91 | [ 92 | {:gogs, "~> 1.0.2"} 93 | ] 94 | end 95 | ``` 96 | 97 | Once you've saved the `mix.exs` file, 98 | run: 99 | ```sh 100 | mix deps.get 101 | ``` 102 | 103 |
104 | 105 | ## Config ⚙️ 106 | 107 | If you are writing tests for a function that relies on `gogs` (and you should!) 108 | then you can add the following line to your `config/test.exs` file: 109 | 110 | ```sh 111 | config :gogs, mock: true 112 | ``` 113 | e.g: [config/test.exs#L2-L4](https://github.com/dwyl/gogs/blob/f6d4658ae2a993aac3a76e812e915680964dfdb5/config/test.exs#L2-L4) 114 | 115 | 116 |
117 | 118 | ## _Setup_ 🔧 119 | 120 | For `gogs` to work 121 | in your `Elixir/Phoenix` App, 122 | you will need to have 123 | a few environment variables defined. 124 | 125 | There are **3 _required_** 126 | and **2 _optional_** variables. 127 | Make sure you read through the next section 128 | to determine if you _need_ the _optional_ ones. 129 | 130 | 131 | ### _Required_ Environment Variables 132 | 133 | > See: [`.env_sample`](https://github.com/dwyl/gogs/blob/main/.env_sample) 134 | 135 | There are **3 _required_** environment variables: 136 | 137 | 1. `GOGS_URL` - the domain where your Gogs Server is deployed, 138 | without the protocol, e.g: `gogs-server.fly.dev` 139 | 140 | 2. `GOGS_ACCESS_TOKEN` - the REST API Access Token 141 | See: https://github.com/dwyl/gogs-server#connect-via-rest-api-https 142 | 143 | 3. `GOGS_SSH_PRIVATE_KEY_PATH` - absolute path to the `id_rsa` file 144 | on your `localhost` or `Phoenix` server instance. 145 | 146 | > @SIMON: this last env var currently not being picked up. 147 | > So it will just use `~/simon/id_rsa` 148 | > You will need to add your `public` key 149 | > to the Gogs instance for this to work on your `localhost` 150 | > see: 151 | > https://github.com/dwyl/gogs-server#add-ssh-key 152 | 153 | 154 | ### _Optional_ Environment Variables 155 | 156 | #### `GOGS_SSH_PORT` 157 | 158 | If your **`Gogs` Server** is configured 159 | with a **_non-standard_ SSH port**, 160 | then you need to define it: 161 | **`GOGS_SSH_PORT`**
162 | e.g: `10022` for our 163 | `Gogs` Server deployed to Fly.io 164 | 165 | You can easily discover the port by either visiting your 166 | Gogs Server Config page:
167 | `https://your-gogs-server.net/admin/config` 168 | 169 | e.g: 170 | https://gogs-server.fly.dev/admin/config 171 | 172 | 173 | 174 | ![gogs-ssh-port-config](https://user-images.githubusercontent.com/194400/167105374-ef36752f-80a7-4a77-8c78-2dda44a132f9.png) 175 | 176 | Or if you don't have admin access to the config page, 177 | simply view the `ssh` clone link on a repo page, 178 | e.g: https://gogs-server.fly.dev/nelsonic/public-repo 179 | 180 | ![gogs-ssh-port-example](https://user-images.githubusercontent.com/194400/167104890-31b06fa0-bd23-4ecb-b680-91c92398b0a7.png) 181 | 182 | In our case the `GOGS_SSH_PORT` e.g: `10022`.
183 | If you don't set it, then `gogs` will assume TCP port **`22`**. 184 | 185 | #### `GIT_TEMP_DIR_PATH` 186 | 187 | If you want to specify a directory where 188 | you want to clone `git` repos to, 189 | create a `GIT_TEMP_DIR_PATH` environment variable. 190 | e.g: 191 | 192 | ```sh 193 | export GIT_TEMP_DIR_PATH=tmp 194 | ``` 195 | 196 | > **Note**: the directory **must _already_ exist**. 197 | > (it won't be created if it's not there ...) 198 | 199 |
200 | 201 | ## Usage 202 | 203 | If you just want to _read_ 204 | the contents of a file hosted on 205 | a `Gogs` Server, 206 | write code similar to this: 207 | 208 | ```elixir 209 | org_name = "myorg" 210 | repo_name = "public-repo" 211 | file_name = "README.md" 212 | {:ok, %HTTPoison.Response{ body: response_body}} = 213 | Gogs.remote_read_raw(org_name, repo_name,file_name) 214 | # use the response_body (plaintext data) 215 | ``` 216 | 217 | This is exactly the use-case presented in our demo app: 218 | [dwyl/**gogs-demo**#4-create-function](https://github.com/dwyl/gogs-demo#4-create-function-to-interact-with-gogs-repo) 219 | 220 | 221 | 222 |
223 | 224 | Here's a more real-world scenario 225 | in 7 easy steps: 226 | 227 | ### 1. _Create_ a New Repo on the Gogs Server 228 | 229 | ```elixir 230 | # Define the params for the remote repository: 231 | org_name = "myorg" 232 | repo_name = "repo-name" 233 | private = false # boolean 234 | # Create the repo! 235 | Gogs.remote_repo_create(org_name, repo_name, private) 236 | ``` 237 | 238 | > ⚠️ **WARNING**: there is currently no way 239 | > to create an Organisation on the `Gogs` Server 240 | > via `REST API` so the `org_name` 241 | > _must_ already exists. 242 | > e.g: https://gogs-server.fly.dev/myorg 243 | > We will be figuring out a workaround shortly ... 244 | > https://github.com/dwyl/gogs/issues/17 245 | 246 | 247 | ### 2. _Clone_ the Repo 248 | 249 | ```elixir 250 | git_repo_url = Gogs.Helpers.remote_url_ssh(org_name, repo_name) 251 | Gogs.clone(git_repo_url) 252 | ``` 253 | 254 | > Provided you have setup the environment variables, 255 | > and your `Elixir/Phoenix` App has write access to the filesystem, 256 | > this should work without any issues. 257 | > We haven't seen any in practice. 258 | > But if you get stuck at this step, 259 | > [open an issue](https://github.com/dwyl/gogs/issues) 260 | 261 | ### 3. _Read_ the Contents of _Local_ (Cloned) File 262 | 263 | Once you've cloned the `Git` Repo from the `Gogs` Server 264 | to the local filesystem of the `Elixir/Phoenix` App, 265 | you can read any file inside it. 266 | 267 | ```elixir 268 | org_name = "myorg" 269 | repo_name = "public-repo" 270 | file_name = "README.md" 271 | {:ok, text} == Gogs.local_file_read(org_name, repo_name, file_name) 272 | ``` 273 | 274 | ### 4. _Write_ to a File 275 | 276 | ```elixir 277 | file_name = "README.md" 278 | text = "Your README.md text" 279 | Gogs.local_file_write_text(org_name, repo_name, file_name, text) 280 | ``` 281 | 282 | This will create a new file if it doesn't already exist. 283 | 284 | ### 5. _Commit_ Changes 285 | 286 | ```elixir 287 | {:ok, msg} = Gogs.commit(org_name, repo_name, 288 | %{message: "your commit message", full_name: "Al Ex", email: "alex@dwyl.co"}) 289 | ``` 290 | 291 | ### 6. _Push_ to `Gogs` Remote 292 | 293 | ```elixir 294 | # Push to Gogs Server this one is easy. 295 | Gogs.push(org_name, repo_name) 296 | ``` 297 | 298 | ### 7. _Confirm_ the File was Update on the Remote repo 299 | 300 | ```elixir 301 | # Confirm the README.md was updated on the remote repo: 302 | {:ok, %HTTPoison.Response{ body: response_body}} = 303 | Gogs.remote_read_raw(org_name, repo_name, file_name) 304 | "Your README.md text" 305 | ``` 306 | 307 | 308 | ## Full Function Reference / Docs? 📖 309 | 310 | Rather than duplicate all the docs here, 311 | please read the complete function reference, 312 | on hexdocs: https://hexdocs.pm/gogs/Gogs.html 313 | 314 |
315 | 316 | ## Tests! 317 | 318 | By default, the tests run with "mocks", 319 | this means that:
320 | 1. Functional tests run faster (0.2 seconds) 321 | 2. Tests that require filesystem access will run on GitHub CI. 322 | 3. We know that functions are appropriately 323 | ["Test Doubled"] 324 | so that a downstream `Elixir/Phoenix` app 325 | can run in `mock: true` and tests will be mocked (and thus _fast_!) 326 | 327 | To alter this setting to run the tests _without_ mocks, 328 | simply change the boolean from: 329 | 330 | ```elixir 331 | config :gogs, mock: true 332 | ``` 333 | 334 | To: 335 | 336 | ```elixir 337 | config :gogs, mock: false 338 | ``` 339 | 340 | You should still see the same output as all the functions should be tested. 341 | 342 | ### Test Coverage 343 | 344 | When you run the command: 345 | 346 | ```sh 347 | mix c 348 | ``` 349 | (an alias for `mix coveralls.html`)
350 | You will see output similar to the following: 351 | 352 | ```sh 353 | Finished in 0.1 seconds (0.1s async, 0.00s sync) 354 | 3 doctests, 27 tests, 0 failures 355 | 356 | Randomized with seed 715101 357 | ---------------- 358 | COV FILE LINES RELEVANT MISSED 359 | 100.0% lib/git_mock.ex 55 7 0 360 | 100.0% lib/gogs.ex 212 41 0 361 | 100.0% lib/helpers.ex 131 17 0 362 | 100.0% lib/http.ex 119 18 0 363 | 100.0% lib/httpoison_mock.ex 124 20 0 364 | [TOTAL] 100.0% 365 | ---------------- 366 | ``` 367 | 368 | If you want to run the tests _without_ mocks (i.e. "end-to-end"), 369 | update the line in `config/test.exs`: 370 | 371 | ```sh 372 | config :gogs, mock: false 373 | ``` 374 | When you run end-to-end tests with coverage tracking: 375 | 376 | ```sh 377 | mix c 378 | ``` 379 | 380 | You should see the same output: 381 | 382 | ```sh 383 | Finished in 5.5 seconds (5.5s async, 0.00s sync) 384 | 3 doctests, 27 tests, 0 failures 385 | 386 | Randomized with seed 388372 387 | ---------------- 388 | COV FILE LINES RELEVANT MISSED 389 | 100.0% lib/git_mock.ex 55 7 0 390 | 100.0% lib/gogs.ex 212 41 0 391 | 100.0% lib/helpers.ex 131 17 0 392 | 100.0% lib/http.ex 119 18 0 393 | 100.0% lib/httpoison_mock.ex 124 20 0 394 | [TOTAL] 100.0% 395 | ---------------- 396 | ``` 397 | 398 | The only difference is the ***time*** it takes to run the test suite.
399 | The outcome (all tests passing and **100% coverage**) should be ***identical***. 400 | 401 | If you add a feature to the package, 402 | please ensure that the tests pass 403 | in both `mock: true` and `mock: false` 404 | so that we know it works in the _real_ world 405 | as well as in the simulated one. 406 | 407 |
408 | 409 | ## Roadmap 410 | 411 | We are aiming to do a 1:1 feature map between GitHub and `Gogs` 412 | so that we can backup our entire organisation, all repos, issues, labels & PRs. 413 | 414 | We aren't there yet 415 | and we might not be for some time. 416 | The order in which we will be working 417 | on fleshing out the features is: 418 | 419 | 1. **Git Diff** - using the `Git` module to determine the changes made to a specific file 420 | between two Git commits/hashes. This will allow us to visualize the changes made 421 | and can therefore _derive_ the contents of a Pull Request 422 | without having the PR feature exposed via the Gogs API. 423 | See: https://github.com/dwyl/gogs/issues/27 424 | 2. **Issues**: https://github.com/gogs/docs-api/tree/master/Issues 425 | + **Comments** - this is the core content of issues. 426 | We need to parse all the data and map it to the fields in `Gogs`. 427 | + **Labels** - the primary metadata we use to categorize our issues, 428 | see: https://github.com/dwyl/labels 429 | + **Milestones** - used to _group_ issues into batches, e.g. a "sprint" or "feature". 430 | 3. **Repo Stats**: Stars, watchers, forks etc. 431 | 4. **_Your_ Feature Request** Here! 432 | Seriously, if you spot a gap in the list of available functions, 433 | something you want/need to use `Gogs` in any a more advanced/custom way, 434 | please open an issue so we can discuss! 435 | 436 | 437 |
438 | 439 | ## I'm _Stuck!_ 🤷 440 | 441 | As always, if anything is unclear 442 | or you are stuck getting this working, 443 | please open an issue! 444 | [github.com/dwyl/gogs/issues](https://github.com/dwyl/gogs/issues/8) 445 | we're here to help! 446 | 447 |
448 | 449 |
450 | 451 |
452 | 453 | # ⚠️ Disclaimer! ⚠️ 454 | 455 | This package is provided "**as is**". 456 | We make ***no guarantee/warranty*** that it _works_.
457 | We _cannot_ be held responsible 458 | for any undesirable effects of it's usage. 459 | e.g: if you use the [`Gogs.delete/1`](https://hexdocs.pm/gogs/Gogs.html#delete/1) 460 | it will _permanently/irrecoverably_ **`delete`** the repo. 461 | Use it with caution! 462 | 463 | With the disclaimer out of the way, 464 | and your expectations clearly set, 465 | here are the facts: 466 | We are using this package in "production". 467 | We rely on it daily and consider it 468 | ["mission critical"](https://en.wikipedia.org/wiki/Mission_critical). 469 | It works for _us_ an and 470 | we have made every effort to document, 471 | test & _maintain_ it. 472 | If you want to use it, go for it! 473 | But please note that we cannot "_support_" your usage 474 | beyond answering questions on GitHub. 475 | And unless you have a commercial agreement with 476 | [dwyl Ltd.] 477 | 478 | If you spot anything that can be improved, 479 | please open an 480 | [issue](https://github.com/dwyl/gogs/issues), 481 | we're very happy to discuss! 482 | 483 | [![feedback welcome](https://img.shields.io/badge/feedback-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/gogs/issues) -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if Mix.env() == :test do 4 | import_config "test.exs" 5 | end 6 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | # set this to false if you want to hit the actual endpoints during development: 3 | config :gogs, mock: System.get_env("GITHUB_WORKSPACE") || true 4 | 5 | # Do not include metadata nor timestamps in testing logs 6 | config :logger, :console, level: :debug, format: "[$level] $message\n" 7 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "coverage_options": { 3 | "minimum_coverage": 100 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/git_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule Gogs.GitMock do 2 | @moduledoc """ 3 | Mock functions to simulate Git commands. 4 | Sadly, this is necessary until we figure out how to get write-access 5 | on GitHub CI. This module is exported for testing convenience/speed 6 | in downstream/dependent apps. 7 | """ 8 | require Logger 9 | 10 | @doc """ 11 | `clone/1` (mock) returns the path of the existing `test-repo` 12 | so that no remote cloning occurs. This is needed for CI and 13 | is used in downstream tests to speed up suite execution. 14 | 15 | ## Examples 16 | iex> GitMock.clone("ssh://gogs-server.fly.dev:10022/myorg/public-repo.git") 17 | {:ok, %Git.Repository{path: "/path/to/public-repo"}} 18 | 19 | iex> GitMock.clone("any-url-containing-the-word-error-to-trigger-failure") 20 | {:error, %Git.Error{message: "git clone error (mock)"}} 21 | """ 22 | @spec clone(String.t() | list(String.t())) :: {:ok, Git.Repository.t()} | {:error, Git.Error} 23 | def clone(url) do 24 | case Useful.typeof(url) do 25 | # e.g: ["ssh://git@gogs.dev/myorg/error-test.git", "tmp/test-repo"] 26 | # recurse using just the url (String) portion of the list: 27 | "list" -> 28 | url |> List.first() |> clone() 29 | 30 | "binary" -> 31 | Logger.info("Gogs.GitMock.clone #{url}") 32 | 33 | if String.contains?(url, "error") do 34 | {:error, %Git.Error{message: "git clone error (mock)"}} 35 | else 36 | {:ok, %Git.Repository{path: Gogs.Helpers.local_repo_path("test-org", "test-repo")}} 37 | end 38 | end 39 | end 40 | 41 | @doc """ 42 | `push/1` (mock) pushes the latest commits on the current branch 43 | to the Gogs remote repository. 44 | 45 | ## Examples 46 | iex> GitMock.push("my-repo") 47 | {:ok, "To ssh://gogs-server.fly.dev:10022/myorg/my-repo.git\n"} 48 | """ 49 | @spec push(Git.Repository.t(), [any]) :: {:ok, any} 50 | def push(%Git.Repository{path: repo_path}, _args) do 51 | Logger.info("Gogs.GitMock.push #{repo_path}") 52 | repo_name = Gogs.Helpers.get_repo_name_from_url(repo_path <> ".git") 53 | {:ok, "To ssh://gogs-server.fly.dev:10022/myorg/#{repo_name}.git\n"} 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/gogs.ex: -------------------------------------------------------------------------------- 1 | defmodule Gogs do 2 | @moduledoc """ 3 | Documentation for the main `Gogs` functions. 4 | This package is an `Elixir` interface to our `Gogs` Server. 5 | It contains all functions we need to create repositories, 6 | clone, add data to files, commit, push and diff. 7 | Some of these functions use `Git` and others use the `REST API`. 8 | We would _obviously_ prefer if everything was one or the other, 9 | but sadly, some things cannot be done via `Git` or `REST` 10 | so we have adopted a "hybrid" approach. 11 | 12 | If anything is unclear, please open an issue: 13 | [github.com/dwyl/**gogs/issues**](https://github.com/dwyl/gogs/issues) 14 | """ 15 | import Gogs.Helpers 16 | require Logger 17 | 18 | @mock Application.compile_env(:gogs, :mock) 19 | Logger.debug("Gogs > config :gogs, mock: #{to_string(@mock)}") 20 | @git (@mock && Gogs.GitMock) || Git 21 | 22 | @doc """ 23 | `inject_git/0` injects a `Git` TestDouble in Tests & CI 24 | so we don't have duplicate mocks in the downstream app. 25 | """ 26 | def inject_git, do: @git 27 | 28 | @doc """ 29 | `remote_repo_create/3` accepts 3 arguments: `org_name`, `repo_name` & `private`. 30 | It creates a repo on the remote `Gogs` instance as defined 31 | by the environment variable `GOGS_URL`. 32 | For convenience it assumes that you only have _one_ `Gogs` instance. 33 | If you have more or different requirements, please share! 34 | """ 35 | @spec remote_repo_create(String.t(), String.t(), boolean) :: {:ok, map} | {:error, any} 36 | def remote_repo_create(org_name, repo_name, private \\ false) do 37 | url = api_base_url() <> "org/#{org_name}/repos" 38 | Logger.info("remote_repo_create api endpoint: #{url}") 39 | 40 | params = %{ 41 | name: repo_name, 42 | private: private, 43 | description: repo_name, 44 | readme: repo_name 45 | } 46 | 47 | Gogs.Http.post(url, params) 48 | end 49 | 50 | @doc """ 51 | `remote_repo_delete/2` accepts two arguments: `org_name` and `repo_name`. 52 | It deletes the repo on the remote `Gogs` instance as defined 53 | by the environment variable `GOGS_URL`. 54 | """ 55 | @spec remote_repo_delete(String.t(), String.t()) :: {:ok, map} | {:error, any} 56 | def remote_repo_delete(org_name, repo_name) do 57 | url = api_base_url() <> "repos/#{org_name}/#{repo_name}" 58 | Logger.info("remote_repo_delete: #{url}") 59 | Gogs.Http.delete(url) 60 | end 61 | 62 | @doc """ 63 | `remote_read_file/4` reads a file from the remote repo. 64 | Accepts 4 arguments: `org_name`, `repo_name`, `file_name` and `branch_name`. 65 | The 4th argument is *optional* and defaults to `"master"` 66 | (the default branch for a repo hosted on `Gogs`). 67 | Makes a `GET` request to the remote `Gogs` instance as defined 68 | by the environment variable `GOGS_URL`. 69 | Returns `{:ok, %HTTPoison.Response{ body: response_body}}` 70 | Uses REST API Endpoint: 71 | ```sh 72 | GET /repos/:username/:reponame/raw/:branchname/:path 73 | ``` 74 | Ref: https://github.com/gogs/docs-api/blob/master/Repositories/Contents.md#get-contents 75 | """ 76 | @spec remote_read_raw(String.t(), String.t(), String.t(), String.t()) :: 77 | {:ok, map} | {:error, any} 78 | def remote_read_raw(org_name, repo_name, file_name, branch_name \\ "master") do 79 | url = api_base_url() <> "repos/#{org_name}/#{repo_name}/raw/#{branch_name}/#{file_name}" 80 | Logger.debug("Gogs.remote_read_raw: #{url}") 81 | Gogs.Http.get_raw(url) 82 | end 83 | 84 | @doc """ 85 | `remote_render_markdown_html/4` uses `Gog` built-in Markdown processor 86 | to render a Markdown Doc e.g. the `README.md` so you can easily use the HTML. 87 | Accepts 4 arguments: `org_name`, `repo_name`, `file_name` and `branch_name`. 88 | The 4th argument is *optional* and defaults to `"master"` 89 | (the default branch for a repo hosted on `Gogs`). 90 | Makes a `GET` request to the remote `Gogs` instance as defined 91 | by the environment variable `GOGS_URL`. 92 | Returns `{:ok, %HTTPoison.Response{ body: response_body}}` 93 | Uses REST API Endpoint: 94 | ```sh 95 | POST /markdown/raw 96 | ``` 97 | Ref: https://github.com/dwyl/gogs/issues/23 98 | """ 99 | @spec remote_render_markdown_html(String.t(), String.t(), String.t(), String.t()) :: 100 | {:ok, map} | {:error, any} 101 | def remote_render_markdown_html(org_name, repo_name, file_name, branch_name \\ "master") do 102 | # First retrieve the Raw Markdown Text we want to render: 103 | {:ok, %HTTPoison.Response{body: raw_markdown}} = 104 | Gogs.remote_read_raw(org_name, repo_name, file_name, branch_name) 105 | 106 | Logger.debug("raw_markdown: #{raw_markdown}") 107 | 108 | url = api_base_url() <> "markdown/raw" 109 | Logger.info("remote_render_markdown_html/4 #{url}") 110 | # temp_context = "https://github.com/gogs/gogs" 111 | # Ask Gogs to redner the Raw Markdown to HTML: 112 | Gogs.Http.post_raw_html(url, raw_markdown) 113 | # I agree, this is clunky ... We wont use it for latency-sensitive apps. 114 | # But it could be useful for a quick/easy Static Site / Blog App. 💭 115 | end 116 | 117 | @doc """ 118 | `clone/1` clones a remote git repository based on `git_repo_url` 119 | returns the path of the _local_ copy of the repository. 120 | """ 121 | @spec clone(String.t()) :: String.t() 122 | def clone(git_repo_url) do 123 | org_name = get_org_name_from_url(git_repo_url) 124 | repo_name = get_repo_name_from_url(git_repo_url) 125 | local_path = local_repo_path(org_name, repo_name) 126 | Logger.info("git clone #{git_repo_url} #{local_path}") 127 | 128 | case inject_git().clone([git_repo_url, local_path]) do 129 | {:ok, %Git.Repository{path: path}} -> 130 | # Logger.info("Cloned repo: #{git_repo_url} to: #{path}") 131 | path 132 | 133 | {:error, git_err} -> 134 | Logger.error("Gogs.clone/1 tried to clone #{git_repo_url}, got: #{git_err.message}") 135 | local_path 136 | end 137 | end 138 | 139 | @doc """ 140 | `local_branch_create/3` creates a branch with the specified name 141 | or defaults to "draft". 142 | """ 143 | @spec local_branch_create(String.t(), String.t()) :: {:ok, map} | {:error, any} 144 | def local_branch_create(org_name, repo_name, branch_name \\ "draft") do 145 | case Git.checkout(local_git_repo(org_name, repo_name), ["-b", branch_name]) do 146 | {:ok, res} -> 147 | {:ok, res} 148 | 149 | {:error, git_err} -> 150 | Logger.error( 151 | "Git.checkout error: #{git_err.message}, #{repo_name} (should not thow error)" 152 | ) 153 | 154 | {:error, git_err.message} 155 | end 156 | end 157 | 158 | @doc """ 159 | `local_file_read/3` reads the raw text from the `file_name`, 160 | params: `org_name`, `repo_name` & `file_name` 161 | """ 162 | @spec local_file_read(String.t(), String.t(), String.t()) :: {:ok, String.t()} | {:error, any()} 163 | def local_file_read(org_name, repo_name, file_name) do 164 | file_path = Path.join([local_repo_path(org_name, repo_name), file_name]) 165 | File.read(file_path) 166 | end 167 | 168 | @doc """ 169 | `local_file_write_text/3` writes the desired `text`, 170 | to the `file_name` in the `repo_name`. 171 | Touches the file in case it doesn't already exist. 172 | """ 173 | @spec local_file_write_text(String.t(), String.t(), String.t(), String.t()) :: 174 | :ok | {:error, any} 175 | def local_file_write_text(org_name, repo_name, file_name, text) do 176 | file_path = Path.join([local_repo_path(org_name, repo_name), file_name]) 177 | Logger.info("attempting to write to #{file_path}") 178 | File.touch!(file_path) 179 | File.write(file_path, text) 180 | end 181 | 182 | @doc """ 183 | `commit/3` commits the latest changes on the local branch. 184 | Accepts the `repo_name` and a `Map` of `params`: 185 | `params.message`: the commit message you want in the log 186 | `params.full_name`: the name of the person making the commit 187 | `params.email`: email address of the person committing. 188 | """ 189 | @spec commit(String.t(), String.t(), map) :: {:ok, any} | {:error, any} 190 | def commit(org_name, repo_name, params) do 191 | repo = local_git_repo(org_name, repo_name) 192 | # Add all files in the repo 193 | {:ok, _output} = Git.add(repo, ["."]) 194 | # Commit with message 195 | {:ok, _output} = 196 | Git.commit(repo, [ 197 | "-m", 198 | params.message, 199 | ~s(--author="#{params.full_name} <#{params.email}>") 200 | ]) 201 | end 202 | 203 | @doc """ 204 | `push/2` pushes the `org_name/repo_name` (current active branch) 205 | to the remote repository URL. Mocked during test/CI. 206 | """ 207 | @spec push(String.t(), String.t()) :: {:ok, any} | {:error, any} 208 | def push(org_name, repo_name) do 209 | # Get the current git branch: 210 | git_repo = local_git_repo(org_name, repo_name) 211 | {:ok, branch} = Git.branch(git_repo, ~w(--show-current)) 212 | # Remove trailing whitespace as Git chokes on it: 213 | branch = String.trim(branch) 214 | # Push the current branch: 215 | inject_git().push(git_repo, ["-u", "origin", branch]) 216 | end 217 | end 218 | -------------------------------------------------------------------------------- /lib/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Gogs.Helpers do 2 | @moduledoc """ 3 | Helper functions that can be unit tested independently of the main functions. 4 | If you spot any way to make these better, please share: 5 | https://github.com/dwyl/gogs/issues 6 | """ 7 | require Logger 8 | # @env_required ~w/GOGS_URL GOGS_SSH_PORT GOGS_ACCESS_TOKEN/ 9 | @cwd File.cwd!() 10 | @git_dir Envar.get("GIT_TEMP_DIR_PATH", @cwd) 11 | @mock Application.compile_env(:gogs, :mock) 12 | 13 | @doc """ 14 | `api_base_url/0` returns the `Gogs` Server REST API url for API requests. 15 | 16 | ## Examples 17 | iex> Gogs.Helpers.api_base_url() 18 | "https://gogs-server.fly.dev/api/v1/" 19 | """ 20 | @spec api_base_url() :: String.t() 21 | def api_base_url do 22 | "https://#{Envar.get("GOGS_URL")}/api/v1/" 23 | end 24 | 25 | @doc """ 26 | `make_url/2` constructs the URL based on the supplied git `url` and TCP `port`. 27 | If the `port` is set it will be a custom Gogs instance. 28 | 29 | ## Examples 30 | iex> Gogs.Helpers.make_url("gogs-server.fly.dev", "10022") 31 | "ssh://git@gogs-server.fly.dev:10022/" 32 | 33 | iex> Gogs.Helpers.make_url("github.com") 34 | "git@github.com:" 35 | 36 | """ 37 | @spec make_url(String.t(), integer() | nil) :: String.t() 38 | def make_url(git_url, port \\ 0) 39 | def make_url(git_url, port) when port > 0, do: "ssh://git@#{git_url}:#{port}/" 40 | def make_url(git_url, _port), do: "git@#{git_url}:" 41 | 42 | @doc """ 43 | `remote_url/3` returns the git remote url. 44 | """ 45 | @spec remote_url(String.t(), String.t(), String.t()) :: String.t() 46 | def remote_url(base_url, org, repo) do 47 | "#{base_url}#{org}/#{repo}.git" 48 | end 49 | 50 | @doc """ 51 | `remote_url_ssh/2` returns the remote ssh url for cloning. 52 | """ 53 | @spec remote_url_ssh(String.t(), String.t()) :: String.t() 54 | def remote_url_ssh(org, repo) do 55 | url = Envar.get("GOGS_URL") 56 | port = Envar.get("GOGS_SSH_PORT") 57 | git_url = Gogs.Helpers.make_url(url, port) 58 | remote_url(git_url, org, repo) 59 | end 60 | 61 | @spec get_org_repo_names(String.t()) :: {String.t(), String.t()} 62 | defp get_org_repo_names(url) do 63 | [org, repo] = 64 | url 65 | |> String.split("/") 66 | |> Enum.take(-2) 67 | 68 | {org, repo} 69 | end 70 | 71 | @doc """ 72 | `get_repo_name_from_url/1` extracts the repository name from a .git url. 73 | Feel free to refactor/simplify this function if you want. 74 | """ 75 | @spec get_repo_name_from_url(String.t()) :: String.t() 76 | def get_repo_name_from_url(url) do 77 | {_org, repo} = get_org_repo_names(url) 78 | String.split(repo, ".git") |> List.first() 79 | end 80 | 81 | @doc """ 82 | `get_org_name_from_url/1` extracts the organisation name from a .git url. 83 | ssh://git@gogs-server.fly.dev:10022/theorg/myrepo.git 84 | """ 85 | @spec get_org_name_from_url(String.t()) :: String.t() 86 | def get_org_name_from_url(url) do 87 | {org, _repo} = get_org_repo_names(url) 88 | org 89 | end 90 | 91 | @doc """ 92 | `local_repo_path/2` returns the full system path for the cloned repo 93 | on the `localhost` i.e. the Elixir/Phoenix server that cloned it. 94 | """ 95 | @spec local_repo_path(String.t(), String.t()) :: binary() 96 | def local_repo_path(org, repo) do 97 | # coveralls-ignore-start 98 | if @mock do 99 | if String.contains?(repo, "no-repo") do 100 | # in branch test we need to simulate a full path not a test-repo one ... 101 | Path.join([temp_dir(@git_dir), org, repo]) |> Path.expand() 102 | else 103 | Path.join([temp_dir(@git_dir), "test-repo"]) |> Path.expand() 104 | end 105 | else 106 | Path.join([temp_dir(@git_dir), org, repo]) |> Path.expand() 107 | end 108 | 109 | # coveralls-ignore-stop 110 | end 111 | 112 | @doc """ 113 | `local_git_repo/2` returns the `%Git.Repository{}` (struct) for an `org` and `repo` 114 | on the `localhost`. This is used by the `Git` module to perform operations. 115 | """ 116 | @spec local_git_repo(String.t(), String.t()) :: Git.Repository.t() 117 | def local_git_repo(org, repo) do 118 | %Git.Repository{path: local_repo_path(org, repo)} 119 | end 120 | 121 | @doc """ 122 | `temp_dir/1` returns the Current Working Directory (CWD). 123 | Made this a function in case we want to change the location of the 124 | directory later e.g. to a temporary directory. 125 | """ 126 | @spec temp_dir(String.t() | nil) :: binary() 127 | def temp_dir(dir \\ nil) do 128 | # Logger.info("temp_dir: #{dir} ") 129 | if dir && File.exists?(dir) do 130 | dir 131 | # coveralls-ignore-start 132 | else 133 | File.cwd!() 134 | # coveralls-ignore-stop 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/http.ex: -------------------------------------------------------------------------------- 1 | defmodule Gogs.Http do 2 | @moduledoc """ 3 | Documentation for the `HTTP` "verb" functions. 4 | Should be self-explanatory. 5 | Each function documented & typespecd. 6 | If anything is unclear, please open an issue: 7 | [github.com/dwyl/**gogs/issues**](https://github.com/dwyl/gogs/issues) 8 | """ 9 | require Logger 10 | 11 | @mock Application.compile_env(:gogs, :mock) 12 | Logger.debug("GogsHttp > config :gogs, mock: #{to_string(@mock)}") 13 | @httpoison (@mock && Gogs.HTTPoisonMock) || HTTPoison 14 | 15 | defp access_token do 16 | Envar.get("GOGS_ACCESS_TOKEN") 17 | end 18 | 19 | defp auth_header do 20 | {"Authorization", "token #{access_token()}"} 21 | end 22 | 23 | defp json_headers do 24 | [ 25 | {"Accept", "application/json"}, 26 | auth_header(), 27 | {"Content-Type", "application/json"} 28 | ] 29 | end 30 | 31 | @doc """ 32 | `inject_poison/0` injects a TestDouble of HTTPoison in Test. 33 | see: https://github.com/dwyl/elixir-auth-google/issues/35 34 | """ 35 | def inject_poison, do: @httpoison 36 | 37 | @doc """ 38 | `parse_body_response/1` parses the response returned by the Gogs Server 39 | so your app can use the resulting JSON. 40 | """ 41 | @spec parse_body_response({atom, String.t()} | {:error, any}) :: {:ok, map} | {:error, any} 42 | def parse_body_response({:error, err}), do: {:error, err} 43 | 44 | def parse_body_response({:ok, response}) do 45 | # Logger.debug(response) # very noisy! 46 | body = Map.get(response, :body) 47 | if body == nil || byte_size(body) == 0 do 48 | Logger.warning("GogsHttp.parse_body_response: response body is nil!") 49 | {:error, :no_body} 50 | else 51 | {:ok, str_key_map} = Jason.decode(body) 52 | # make keys of map atoms for easier access in templates etc. 53 | {:ok, Useful.atomize_map_keys(str_key_map)} 54 | end 55 | end 56 | 57 | @doc """ 58 | `get/1` accepts one argument: `url` the REST API endpoint. 59 | Makes an `HTTP GET` request to the specified `url`. 60 | Auth Headers and Content-Type are implicit. 61 | returns `{:ok, map}` 62 | """ 63 | @spec get(String.t()) :: {:ok, map} | {:error, any} 64 | def get(url) do 65 | Logger.debug("GogsHttp.get #{url}") 66 | inject_poison().get(url, json_headers()) 67 | |> parse_body_response() 68 | end 69 | 70 | @doc """ 71 | `get_raw/1` as it's name suggests gets the raw data 72 | (expects the reponse to be plaintext not JSON) 73 | accepts one argument: `url` the REST API endpoint. 74 | Makes an `HTTP GET` request to the specified `url`. 75 | Auth Headers and Content-Type are implicit. 76 | returns `{:ok, map}` 77 | """ 78 | @spec get_raw(String.t()) :: {:ok, map} | {:error, any} 79 | def get_raw(url) do 80 | Logger.debug("GogsHttp.get_raw #{url}") 81 | inject_poison().get(url, [auth_header()]) 82 | end 83 | 84 | @doc """ 85 | `post_raw_html/2` accepts two arguments: `url` and `raw_markdown`. 86 | Makes an `HTTP POST` request to the specified `url` 87 | passing in the `params` as the request body. 88 | Does NOT attempt to parse the response body as JSON. 89 | Auth Headers and Content-Type are implicit. 90 | """ 91 | @spec post_raw_html(String.t(), String.t()) :: {:ok, String.t()} | {:error, any} 92 | def post_raw_html(url, raw_markdown) do 93 | Logger.debug("GogsHttp.post_raw #{url}") 94 | # Logger.debug("raw_markdown: #{raw_markdown}") 95 | headers = [ 96 | {"Accept", "text/html"}, 97 | auth_header(), 98 | ] 99 | inject_poison().post(url, raw_markdown, headers) 100 | end 101 | 102 | @doc """ 103 | `post/2` accepts two arguments: `url` and `params`. 104 | Makes an `HTTP POST` request to the specified `url` 105 | passing in the `params` as the request body. 106 | Auth Headers and Content-Type are implicit. 107 | """ 108 | @spec post(String.t(), map) :: {:ok, map} | {:error, any} 109 | def post(url, params \\ %{}) do 110 | Logger.debug("GogsHttp.post #{url}") 111 | body = Jason.encode!(params) 112 | inject_poison().post(url, body, json_headers()) 113 | |> parse_body_response() 114 | end 115 | 116 | @doc """ 117 | `delete/1` accepts a single argument `url`; 118 | the `url` for the repository to be deleted. 119 | """ 120 | @spec delete(String.t()) :: {:ok, map} | {:error, any} 121 | def delete(url) do 122 | Logger.debug("GogsHttp.delete #{url}") 123 | inject_poison().delete(url <> "?token=#{access_token()}") 124 | |> parse_body_response() 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/httpoison_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule Gogs.HTTPoisonMock do 2 | @moduledoc """ 3 | Mock (stub) our API requests to the Gogs API 4 | so that we can test all of our code (with Mocks) on GitHub CI. 5 | These functions pattern match on the params 6 | and return the expected responses. 7 | If you know of a better way of doing this 8 | (preferably without introducing more dependencies ...) 9 | Please share: 10 | [github.com/dwyl/**gogs/issues**](https://github.com/dwyl/gogs/issues) 11 | """ 12 | require Logger 13 | @remote_repo_create_response_body %{ 14 | clone_url: "https://gogs-server.fly.dev/myorg/replacethis.git", 15 | # created_at: "0001-01-01T00:00:00Z", 16 | # default_branch: "public-repo", 17 | # description: "replacethis", 18 | empty: false, 19 | fork: false, 20 | forks_count: 0, 21 | full_name: "myorg/replacethis", 22 | html_url: "https://gogs-server.fly.dev/myorg/replacethis", 23 | # id: 42, 24 | mirror: false, 25 | name: "replacethis", 26 | open_issues_count: 0, 27 | owner: %{ 28 | avatar_url: "https://gogs-server.fly.dev/avatars/2", 29 | email: "", 30 | full_name: "", 31 | id: 2, 32 | login: "myorg", 33 | username: "myorg" 34 | }, 35 | parent: nil, 36 | permissions: %{admin: true, pull: true, push: true}, 37 | private: false, 38 | # size: 0, 39 | ssh_url: "ssh://git@gogs-server.fly.dev:10022/myorg/replacethis.git", 40 | stars_count: 0, 41 | # updated_at: "0001-01-01T00:00:00Z", 42 | # watchers_count: 0, 43 | website: "" 44 | } 45 | 46 | # make a valid response body for testing 47 | def make_repo_create_post_response_body(repo_name) do 48 | Map.merge(@remote_repo_create_response_body, %{ 49 | clone_url: "https://gogs-server.fly.dev/myorg/#{repo_name}.git", 50 | # description: repo_name, 51 | full_name: "myorg/#{repo_name}", 52 | html_url: "https://gogs-server.fly.dev/myorg/#{repo_name}", 53 | ssh_url: "ssh://git@gogs-server.fly.dev:10022/myorg/#{repo_name}.git", 54 | # readme: repo_name, 55 | name: repo_name 56 | }) 57 | end 58 | 59 | @raw_response {:ok, %HTTPoison.Response{ 60 | body: "# public-repo\n\nplease don't update this. the tests read it.", 61 | status_code: 200 62 | }} 63 | 64 | @doc """ 65 | `get/2` mocks the HTTPoison.get/2 function when parameters match test vars. 66 | Feel free refactor this if you can make it pretty. 67 | """ 68 | def get(url, _headers) do 69 | Logger.debug("Gogs.HTTPoisonMock.get/2 #{url}") 70 | case String.contains?(url, "/raw/") do 71 | true -> 72 | @raw_response 73 | false -> 74 | repo_name = Gogs.Helpers.get_repo_name_from_url(url) 75 | response_body = 76 | make_repo_create_post_response_body(repo_name) 77 | |> Jason.encode!() 78 | {:ok, %HTTPoison.Response{body: response_body, status_code: 200}} 79 | end 80 | end 81 | 82 | @doc """ 83 | `post/3` mocks the HTTPoison.post/3 function when parameters match test vars. 84 | Feel free refactor this if you can make it pretty. 85 | """ 86 | def post(url, body, headers) do 87 | Logger.debug("Gogs.HTTPoisonMock.post/3 #{url}") 88 | if String.contains?(url, "markdown/raw") do 89 | post_raw_html(url, body, headers) 90 | else 91 | body_map = Jason.decode!(body) |> Useful.atomize_map_keys() 92 | response_body = 93 | make_repo_create_post_response_body(body_map.name) 94 | |> Jason.encode!() 95 | {:ok, %HTTPoison.Response{body: response_body, status_code: 200}} 96 | end 97 | end 98 | 99 | def raw_html do 100 | "

public-repo

\n\n

please don't update this. the tests read it.

\n" 101 | end 102 | 103 | @doc """ 104 | `post_raw/3` mocks the GogsHttp.post_raw/3 function. 105 | Feel free refactor this if you can make it pretty. 106 | """ 107 | def post_raw_html(url, _body, _headers) do 108 | Logger.debug("Gogs.HTTPoisonMock.post_raw/3 #{url}") 109 | response_body = raw_html() 110 | {:ok, %HTTPoison.Response{body: response_body, status_code: 200}} 111 | end 112 | 113 | @doc """ 114 | `delete/1` mocks the HTTPoison `delete` function. 115 | Feel free refactor this if you can make it pretty. 116 | """ 117 | def delete(url) do 118 | Logger.debug("Gogs.HTTPoisonMock.delete/1 #{url}") 119 | {:ok, %HTTPoison.Response{ 120 | body: Jason.encode!(%{deleted: List.first(String.split(url, "?"))}), 121 | status_code: 200 122 | }} 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Gogs.MixProject do 2 | use Mix.Project 3 | 4 | @elixir_requirement "~> 1.9" 5 | 6 | def project do 7 | [ 8 | app: :gogs, 9 | version: "1.1.0", 10 | elixir: @elixir_requirement, 11 | start_permanent: Mix.env() == :prod, 12 | deps: deps(), 13 | test_coverage: [tool: ExCoveralls], 14 | preferred_cli_env: [ 15 | c: :test, 16 | coveralls: :test, 17 | "coveralls.html": :test, 18 | "coveralls.json": :test 19 | ], 20 | aliases: aliases(), 21 | deps: deps(), 22 | package: package(), 23 | description: "Simple Elixir interface with a Gogs (Git) Server" 24 | ] 25 | end 26 | 27 | # Run "mix help compile.app" to learn about applications. 28 | def application do 29 | [ 30 | extra_applications: [:logger] 31 | ] 32 | end 33 | 34 | # Run "mix help deps" to learn about dependencies. 35 | defp deps do 36 | [ 37 | # Make HTTP Requests: github.com/edgurgel/httpoison 38 | {:httpoison, "~> 2.2.0"}, 39 | 40 | # Parse JSON data: github.com/michalmuskala/jason 41 | {:jason, "~> 1.4.0"}, 42 | 43 | # Check environment variables: github.com/dwyl/envar 44 | {:envar, "~> 1.1.0"}, 45 | 46 | # Git interface: github.com/danhper/elixir-git-cli 47 | {:git_cli, "~> 0.3"}, 48 | 49 | # Useful functions: github.com/dwyl/useful 50 | {:useful, "~> 1.14.0"}, 51 | 52 | # Check test coverage: github.com/parroty/excoveralls 53 | {:excoveralls, "~> 0.18.0", only: :test}, 54 | 55 | # Create Documentation for publishing Hex.docs: 56 | {:ex_doc, "~> 0.28", only: :dev}, 57 | 58 | # Keep Code Tidy: https://github.com/rrrene/credo 59 | {:credo, "~> 1.7.0", only: [:dev, :test], runtime: false}, 60 | {:dialyxir, "~> 1.1", only: [:dev], runtime: false} 61 | ] 62 | end 63 | 64 | defp aliases do 65 | [ 66 | c: ["coveralls.html"] 67 | ] 68 | end 69 | 70 | defp package() do 71 | [ 72 | files: ~w(lib LICENSE mix.exs README.md test-repo), 73 | name: "gogs", 74 | licenses: ["GPL-2.0-or-later"], 75 | maintainers: ["dwyl"], 76 | links: %{"GitHub" => "https://github.com/dwyl/gogs"} 77 | ] 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, 3 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 4 | "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, 5 | "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, 6 | "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, 7 | "envar": {:hex, :envar, "1.1.0", "105bcac5a03800a1eb21e2c7e229edc687359b0cc184150ec1380db5928c115c", [:mix], [], "hexpm", "97028ab4a040a5c19e613fdf46a41cf51c6e848d99077e525b338e21d2993320"}, 8 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, 9 | "ex_doc": {:hex, :ex_doc, "0.31.0", "06eb1dfd787445d9cab9a45088405593dd3bb7fe99e097eaa71f37ba80c7a676", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5350cafa6b7f77bdd107aa2199fe277acf29d739aba5aee7e865fc680c62a110"}, 10 | "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, 11 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, 12 | "git_cli": {:hex, :git_cli, "0.3.0", "a5422f9b95c99483385b976f5d43f7e8233283a47cda13533d7c16131cb14df5", [:mix], [], "hexpm", "78cb952f4c86a41f4d3511f1d3ecb28edb268e3a7df278de2faa1bd4672eaf9b"}, 13 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~>2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 14 | "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, 15 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 16 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 17 | "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, 18 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, 19 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.3", "d684f4bac8690e70b06eb52dad65d26de2eefa44cd19d64a8095e1417df7c8fd", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "b78dc853d2e670ff6390b605d807263bf606da3c82be37f9d7f68635bd886fc9"}, 20 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 21 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 22 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 23 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, 24 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 25 | "plug": {:hex, :plug, "1.15.1", "b7efd81c1a1286f13efb3f769de343236bd8b7d23b4a9f40d3002fc39ad8f74c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "459497bd94d041d98d948054ec6c0b76feacd28eec38b219ca04c0de13c79d30"}, 26 | "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, 27 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 28 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 29 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 30 | "useful": {:hex, :useful, "1.14.0", "78a2dbeafe1a805ce6d430bbaddf52c64ec7aed06b5b5ebb4e8dbd8b885f4808", [:mix], [{:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "0e8fe3a0843b0201cb556fbce9ead0f9b1dc5cf3afe00b52a4d6789654cca588"}, 31 | } 32 | -------------------------------------------------------------------------------- /test/git_mock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GogsGitMockTest do 2 | use ExUnit.Case, async: true 3 | # This file is just for exercising the mock functions 4 | # Ignore it 5 | 6 | # can you think of a better way of testing/simulating this error condition? 7 | test "Gogs.GitMock.clone {:error, %Git.Error{message}}" do 8 | repo = "error" 9 | org = "myorg" 10 | url = Gogs.Helpers.remote_url_ssh(org, repo) 11 | {:error, %Git.Error{message: response_message}} = Gogs.GitMock.clone(url) 12 | expected = "git clone error (mock)" 13 | assert expected == response_message 14 | end 15 | 16 | test "Gogs.GitMock.clone returns {:ok, %Git.Repository{path: local_path}}" do 17 | expected = Gogs.Helpers.local_repo_path("test-org", "test-repo") 18 | {:ok, %Git.Repository{path: local_path}} = Gogs.GitMock.clone("any-url") 19 | assert expected == local_path 20 | end 21 | 22 | test "Gogs.GitMock.clone with list recurses using the first param as url" do 23 | expected = Gogs.Helpers.local_repo_path("test-org", "test-repo") 24 | 25 | {:ok, %Git.Repository{path: local_path}} = 26 | Gogs.GitMock.clone(["any-url", "/path/to/local/repo"]) 27 | 28 | assert expected == local_path 29 | end 30 | 31 | test "Gogs.GitMock.push mocks pushing to a remote repo" do 32 | org_name = "test-org" 33 | repo_name = "test-repo" 34 | expected = "To ssh://gogs-server.fly.dev:10022/myorg/#{repo_name}.git\n" 35 | git_repo = %Git.Repository{path: Gogs.Helpers.local_repo_path(org_name, repo_name)} 36 | {:ok, msg} = Gogs.GitMock.push(git_repo, ["any"]) 37 | assert expected == msg 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/gogs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GogsTest do 2 | use ExUnit.Case, async: true 3 | require Logger 4 | doctest Gogs 5 | # Let's do this!! 🚀 6 | @cwd File.cwd!() 7 | @git_dir Envar.get("GIT_TEMP_DIR_PATH", @cwd) 8 | @mock Application.compile_env(:gogs, :mock) 9 | 10 | # https://elixirforum.com/t/random-unisgned-64-bit-integers/31659 11 | # e.g: "43434105246416498" 12 | defp random_postive_int_str() do 13 | :crypto.strong_rand_bytes(7) |> :binary.decode_unsigned() |> Integer.to_string() 14 | end 15 | 16 | # Create a test repo with the name "test-repo123" 17 | defp test_repo() do 18 | "test-repo" <> random_postive_int_str() 19 | end 20 | 21 | defp create_test_git_repo(org_name) do 22 | repo_name = test_repo() 23 | Gogs.remote_repo_create(org_name, repo_name, false) 24 | git_repo_url = Gogs.Helpers.remote_url_ssh(org_name, repo_name) 25 | # Logger.debug("create_test_git_repo/1 git_repo_url: #{git_repo_url}") 26 | Gogs.clone(git_repo_url) 27 | 28 | repo_name 29 | end 30 | 31 | # Cleanup helper functions 32 | defp delete_local_directory(repo_name) do 33 | path = Path.join(Gogs.Helpers.temp_dir(@git_dir), repo_name) 34 | Logger.debug("GogsTest.delete_local_directory: #{path}") 35 | File.rm_rf(path) 36 | end 37 | 38 | defp teardown_local_and_remote(org_name, repo_name) do 39 | delete_local_directory(repo_name) 40 | Gogs.remote_repo_delete(org_name, repo_name) 41 | end 42 | 43 | test "remote_repo_create/3 creates a new repo on the Gogs server" do 44 | org_name = "myorg" 45 | repo_name = test_repo() 46 | {:ok, response} = Gogs.remote_repo_create(org_name, repo_name, false) 47 | response = Map.drop(response, [:id, :created_at, :updated_at]) 48 | 49 | mock_response = Gogs.HTTPoisonMock.make_repo_create_post_response_body(repo_name) 50 | assert response.name == mock_response.name 51 | 52 | # Cleanup: 53 | teardown_local_and_remote(org_name, repo_name) 54 | end 55 | 56 | test "Gogs.remote_read_raw/4 retrieves the contents of the README.md file" do 57 | org_name = "myorg" 58 | repo_name = "public-repo" 59 | file_name = "README.md" 60 | 61 | {:ok, %HTTPoison.Response{body: response_body}} = 62 | Gogs.remote_read_raw(org_name, repo_name, file_name) 63 | 64 | expected = "# public-repo\n\nplease don't update this. the tests read it." 65 | assert expected == response_body 66 | end 67 | 68 | test "Gogs.remote_render_markdown_html/4 renders README.md file as HTML!" do 69 | org_name = "myorg" 70 | repo_name = "public-repo" 71 | file_name = "README.md" 72 | 73 | {:ok, %HTTPoison.Response{body: response_body}} = 74 | Gogs.remote_render_markdown_html(org_name, repo_name, file_name) 75 | 76 | # IO.inspect(response_body) 77 | assert Gogs.HTTPoisonMock.raw_html() == response_body 78 | end 79 | 80 | test "Gogs.clone clones a known remote repository Gogs on Fly.io" do 81 | org = "nelsonic" 82 | repo = "public-repo" 83 | git_repo_url = Gogs.Helpers.remote_url_ssh(org, repo) 84 | 85 | path = Gogs.clone(git_repo_url) 86 | # assert path == Gogs.Helpers.local_repo_path(repo) 87 | 88 | # Attempt to clone it a second time to test the :error branch: 89 | path2 = Gogs.clone(git_repo_url) 90 | assert path == path2 91 | 92 | # Clean up (but don't delete the remote repo!!) 93 | delete_local_directory("public-repo") 94 | end 95 | 96 | test "Gogs.clone error (simulate unhappy path)" do 97 | repo = "error" 98 | org = "myorg" 99 | git_repo_url = Gogs.Helpers.remote_url_ssh(org, repo) 100 | path = Gogs.clone(git_repo_url) 101 | assert path == Gogs.Helpers.local_repo_path(org, repo) 102 | end 103 | 104 | test "local_branch_create/1 creates a new branch on the localhost" do 105 | org_name = "myorg" 106 | repo_name = create_test_git_repo(org_name) 107 | # # delete draft branch if exists: 108 | Git.branch(Gogs.Helpers.local_git_repo(org_name, repo_name), ["-m", repo_name]) 109 | Git.branch(Gogs.Helpers.local_git_repo(org_name, repo_name), ~w(-d draft)) 110 | 111 | {:ok, res} = Gogs.local_branch_create(org_name, repo_name, "draft") 112 | assert res == "Switched to a new branch 'draft'\n" 113 | 114 | # Cleanup! 115 | teardown_local_and_remote(org_name, repo_name) 116 | end 117 | 118 | test "local_branch_create/2 returns error if repo doesn't exist" do 119 | repo_name = "no-repo-" <> random_postive_int_str() 120 | org_name = "no-org-" <> random_postive_int_str() 121 | {:error, error} = Gogs.local_branch_create(org_name, repo_name, "draft") 122 | # Unfortunately the error message depends on Git version, 123 | # so we cannot assert the contents of the error message. 124 | # but we know from the pattern match above that it did error. 125 | Logger.error("test: local_branch_create/1 > error: #{error}") 126 | end 127 | 128 | test "write_text_to_file/3 writes text to a specified file" do 129 | org_name = "myorg" 130 | repo_name = create_test_git_repo(org_name) 131 | file_name = "README.md" 132 | text = "text #{repo_name}" 133 | 134 | assert :ok == 135 | Gogs.local_file_write_text(org_name, repo_name, file_name, text) 136 | 137 | # Confirm the text was written to the file: 138 | assert {:ok, text} == Gogs.local_file_read(org_name, repo_name, file_name) 139 | 140 | # Cleanup! 141 | teardown_local_and_remote(org_name, repo_name) 142 | end 143 | 144 | test "commit/2 creates a commit in the repo" do 145 | org_name = "myorg" 146 | repo_name = create_test_git_repo(org_name) 147 | file_name = "README.md" 148 | 149 | assert :ok == 150 | Gogs.local_file_write_text(org_name, repo_name, file_name, "text #{repo_name}") 151 | 152 | # Confirm the text was written to the file: 153 | file_path = Path.join([Gogs.Helpers.local_repo_path(org_name, repo_name), file_name]) 154 | assert {:ok, "text #{repo_name}"} == File.read(file_path) 155 | 156 | {:ok, msg} = 157 | Gogs.commit(org_name, repo_name, %{message: "test msg", full_name: "Al Ex", email: "c@t.co"}) 158 | 159 | assert String.contains?(msg, "test msg") 160 | assert String.contains?(msg, "1 file changed, 1 insertion(+)") 161 | 162 | # Cleanup! 163 | teardown_local_and_remote(org_name, repo_name) 164 | end 165 | 166 | test "Gogs.push/2 pushes the commit to the remote repo" do 167 | org_name = "myorg" 168 | repo_name = create_test_git_repo(org_name) 169 | file_name = "README.md" 170 | text = "text #{repo_name}" 171 | 172 | assert :ok == 173 | Gogs.local_file_write_text(org_name, repo_name, file_name, text) 174 | 175 | # Confirm the text was written to the file: 176 | file_path = Path.join([Gogs.Helpers.local_repo_path(org_name, repo_name), file_name]) 177 | assert {:ok, "text #{repo_name}"} == File.read(file_path) 178 | 179 | # Commit the updated text: 180 | {:ok, msg} = 181 | Gogs.commit(org_name, repo_name, %{message: "test msg", full_name: "Al Ex", email: "c@t.co"}) 182 | 183 | assert String.contains?(msg, "test msg") 184 | 185 | #  Push to Gogs Server! 186 | Gogs.push(org_name, repo_name) 187 | 188 | # Confirm the README.md of the newly created repo was updated: 189 | {:ok, %HTTPoison.Response{body: response_body}} = 190 | Gogs.remote_read_raw(org_name, repo_name, file_name) 191 | 192 | if @mock do 193 | assert response_body == 194 | "# public-repo\n\nplease don't update this. the tests read it." 195 | else 196 | assert response_body == text 197 | end 198 | 199 | # Cleanup! 200 | teardown_local_and_remote(org_name, repo_name) 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /test/gogshelpers_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Gogs.HelpersTest do 2 | use ExUnit.Case, async: true 3 | doctest Gogs.Helpers 4 | 5 | test "Gogs.Helpers.api_base_url/0 returns the API URL for the Gogs Server" do 6 | assert Gogs.Helpers.api_base_url() == "https://gogs-server.fly.dev/api/v1/" 7 | end 8 | 9 | test "temp_dir/1 returns cwd if no dir supplied" do 10 | dir = Envar.get("GITHUB_WORKSPACE", "tmp") 11 | assert Gogs.Helpers.temp_dir(dir) == dir 12 | end 13 | 14 | test "make_url/2 (without port) returns a valid GitHub Base URL" do 15 | url = "github.com" 16 | git_url = Gogs.Helpers.make_url(url) 17 | assert git_url == "git@github.com:" 18 | end 19 | 20 | test "make_url/2 returns a valid Gogs (Fly.io) Base URL" do 21 | git_url = Gogs.Helpers.make_url("gogs-server.fly.dev", "10022") 22 | assert git_url == "ssh://git@gogs-server.fly.dev:10022/" 23 | end 24 | 25 | test "remote_url/3 returns a valid Gogs (Fly.io) remote URL" do 26 | git_url = Gogs.Helpers.make_url("gogs-server.fly.dev", "10022") 27 | remote_url = Gogs.Helpers.remote_url(git_url, "nelsonic", "public-repo") 28 | assert remote_url == "ssh://git@gogs-server.fly.dev:10022/nelsonic/public-repo.git" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/gogshttp_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GogsHttpTest do 2 | use ExUnit.Case, async: true 3 | # Function tests that work both when mock: true and false! 4 | 5 | # can you think of a better way of testing/simulating this error condition? 6 | test "GogsHttp.parse_body_response({:error, err})" do 7 | assert Gogs.Http.parse_body_response({:error, "err"}) == {:error, "err"} 8 | end 9 | 10 | # We've seen an empty body returned in practice. But how to test it ...? 11 | test "GogsHttp.parse_body_response({:ok, response}) with empty/nil body" do 12 | res = %{body: ""} 13 | assert Gogs.Http.parse_body_response({:ok, res}) == {:error, :no_body} 14 | end 15 | 16 | test "GogsHttp.get/1 gets (or mocks) an HTTP GET request to Gogs Server" do 17 | repo_name = "public-repo" 18 | url = "https://gogs-server.fly.dev/api/v1/repos/myorg/#{repo_name}" 19 | {:ok, response} = Gogs.Http.get(url) 20 | # remove unpredictable fields from response when mock:false 21 | drop_fields = ~w(created_at default_branch description id readme size updated_at watchers_count)a 22 | response = Map.drop(response, drop_fields) 23 | mock_response = Gogs.HTTPoisonMock.make_repo_create_post_response_body(repo_name) 24 | assert response == mock_response 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/httpoison_mock_test.exs: -------------------------------------------------------------------------------- 1 | defmodule HttPoisonMockTest do 2 | use ExUnit.Case, async: true 3 | # Mock function tests that work both when mock: true and false! 4 | 5 | test "Gogs.HTTPoisonMock.get /raw/ returns 200" do 6 | {:ok, %HTTPoison.Response{status_code: status}} = 7 | Gogs.HTTPoisonMock.get("/raw/", "any-header") 8 | 9 | assert status == 200 10 | end 11 | 12 | test "Gogs.HTTPoisonMock.get any url should return status 200" do 13 | {:ok, %HTTPoison.Response{status_code: status}} = 14 | Gogs.HTTPoisonMock.get("org/any", "any-header") 15 | 16 | assert status == 200 17 | end 18 | 19 | test "Gogs.HTTPoisonMock.post any url should return status 200" do 20 | {:ok, %HTTPoison.Response{status_code: status, body: resp_body}} = 21 | Gogs.HTTPoisonMock.post("hi", Jason.encode!(%{name: "simon"}), "any-header") 22 | 23 | assert status == 200 24 | body_map = Jason.decode!(resp_body) |> Useful.atomize_map_keys() 25 | assert body_map.full_name == "myorg/simon" 26 | end 27 | 28 | test "Gogs.HTTPoisonMock.delete any url should return status 200" do 29 | {:ok, %HTTPoison.Response{status_code: status}} = Gogs.HTTPoisonMock.delete("any?url") 30 | assert status == 200 31 | end 32 | 33 | test "Gogs.HTTPoisonMock.post when url is markdown/raw should return status 200" do 34 | {:ok, %HTTPoison.Response{status_code: status, body: body}} = 35 | Gogs.HTTPoisonMock.post("markdown/raw", "any", "any") 36 | assert status == 200 37 | assert body == Gogs.HTTPoisonMock.raw_html() 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------