├── .github └── workflows │ ├── linter.yaml │ ├── release.yaml │ ├── tag.yaml │ └── test.yaml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── build └── Dockerfile ├── cmd └── go-assured │ ├── README.md │ ├── main.go │ └── preload_reference.md ├── go.mod ├── go.sum └── pkg └── assured ├── bindings_test.go ├── call.go ├── call_store.go ├── call_test.go ├── client.go ├── client_test.go ├── handlers.go ├── options.go ├── options_test.go ├── routes.go └── testdata ├── assured.json ├── callbacks.json ├── calls.json ├── image.jpg ├── localhost-key.pem └── localhost.pem /.github/workflows/linter.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Code Base 2 | on: 3 | push: 4 | branches-ignore: 5 | - "main" 6 | 7 | jobs: 8 | build: 9 | name: Lint Code Base 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Code 13 | uses: actions/checkout@v3 14 | 15 | - name: Lint Code Base 16 | uses: docker://github/super-linter:v4.9.6 17 | env: 18 | VALIDATE_ALL_CODEBASE: false 19 | VALIDATE_GO: true 20 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release Workflow 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | build: 7 | name: release 8 | env: 9 | REF: ${{ github.ref }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Build 16 | run: | 17 | docker build --file ./build/Dockerfile \ 18 | -t docker.pkg.github.com/jesse0michael/go-rest-assured/assured:${REF##*/} \ 19 | -t docker.pkg.github.com/jesse0michael/go-rest-assured/assured . 20 | 21 | - name: Push 22 | run: | 23 | docker login docker.pkg.github.com -u Jesse0Michael -p ${{ secrets.PAT }} 24 | docker push docker.pkg.github.com/jesse0michael/go-rest-assured/assured:${REF##*/} 25 | docker push docker.pkg.github.com/jesse0michael/go-rest-assured/assured 26 | -------------------------------------------------------------------------------- /.github/workflows/tag.yaml: -------------------------------------------------------------------------------- 1 | name: Tag 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build: 9 | name: tag 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Semantic Release 16 | uses: ridedott/release-me-action@master 17 | with: 18 | release-branches: '["main"]' 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.PAT }} 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | 5 | jobs: 6 | test: 7 | name: test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup Go 13 | uses: actions/setup-go@v4 14 | with: 15 | go-version: stable 16 | check-latest: true 17 | 18 | - name: Test 19 | run: | 20 | go mod download 21 | mkdir .coverage 22 | go test -v ./pkg/... -cover -coverprofile=.coverage/assured.coverprofile 23 | 24 | - name: Coveralls 25 | uses: shogo82148/actions-goveralls@v1 26 | with: 27 | path-to-profile: .coverage/assured.coverprofile 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .coverage/ 3 | vendor/ 4 | .vscode/ 5 | debug 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [4.4.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.3.0...v4.4.0) (2024-06-29) 2 | 3 | ### Features 4 | 5 | - add healthcheck ([15d5429](https://github.com/Jesse0Michael/go-rest-assured/commit/15d542972eb216f0cf1d4977ba0c5862558ab41a)) 6 | 7 | # [4.3.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.2.0...v4.3.0) (2024-02-28) 8 | 9 | ### Features 10 | 11 | - remove go-kit/kit and gorilla/mux in favor of net/http's ServeMux ([30da74d](https://github.com/Jesse0Michael/go-rest-assured/commit/30da74da27cea59b735a47a6083502cd734ad10c)) 12 | 13 | # [4.2.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.1.0...v4.2.0) (2024-02-23) 14 | 15 | ### Features 16 | 17 | - add custom http verb support ([cf62869](https://github.com/Jesse0Michael/go-rest-assured/commit/cf62869d6ef1ba9af66039a541c296fd19f55ade)) 18 | 19 | # [4.1.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.0.3...v4.1.0) (2023-12-05) 20 | 21 | ### Features 22 | 23 | - support passing a slog.logger ([902baf0](https://github.com/Jesse0Michael/go-rest-assured/commit/902baf096722ac65a444b6493cd49e9e2630e382)) 24 | 25 | ## [4.0.3](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.0.2...v4.0.3) (2023-11-29) 26 | 27 | ### Bug Fixes 28 | 29 | - skip empty options ([5afb731](https://github.com/Jesse0Michael/go-rest-assured/commit/5afb7319137610ce04d441ea0a0b26cf5d300d2a)) 30 | 31 | ## [4.0.2](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.0.1...v4.0.2) (2023-09-15) 32 | 33 | ### Chores 34 | 35 | - change default branch to main ([3eca768](https://github.com/Jesse0Michael/go-rest-assured/commit/3eca7685cab2b410ecedac6e40c9b63a82e1c30b)) 36 | 37 | ### Continuous Integration 38 | 39 | - release from main ([8e01820](https://github.com/Jesse0Michael/go-rest-assured/commit/8e018207b427df9a7eb6b2fc754e611fdd38929a)) 40 | 41 | ## [4.0.1](https://github.com/Jesse0Michael/go-rest-assured/compare/v4.0.0...v4.0.1) (2023-09-15) 42 | 43 | ### Build System 44 | 45 | - move docker labels ([be7180e](https://github.com/Jesse0Michael/go-rest-assured/commit/be7180eeffa882731ade7397e8660d7c738de932)) 46 | 47 | # [3.2.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v3.1.2...v3.2.0) (2023-09-15) 48 | 49 | ### Bug Fixes 50 | 51 | - use google/uuid package ([f440642](https://github.com/Jesse0Michael/go-rest-assured/commit/f44064258ee2405495f717e34f08892a6d231dc7)) 52 | 53 | ### Build System 54 | 55 | - add docker labels ([5abe583](https://github.com/Jesse0Michael/go-rest-assured/commit/5abe58315dd85d4b90d4a7940ce1ba472f399219)) 56 | 57 | ### Chores 58 | 59 | - update license ([ebce1b2](https://github.com/Jesse0Michael/go-rest-assured/commit/ebce1b2bac5fe337a8604a563096912527490ebf)) 60 | - upgrade to go 1.21 ([7adaa3f](https://github.com/Jesse0Michael/go-rest-assured/commit/7adaa3fbe3ad4ad6db034be15d423fb7776e0b46)) 61 | 62 | ### Continuous Integration 63 | 64 | - update go version ([2ea667c](https://github.com/Jesse0Michael/go-rest-assured/commit/2ea667c69a388c85f5bee6164c76d3b39c9d3ac7)) 65 | 66 | ### Features 67 | 68 | - add NewClientServe function ([453d258](https://github.com/Jesse0Michael/go-rest-assured/commit/453d25892330d7520f57dd12e3c550cc4ff2285a)) 69 | - BREAKING CHANGE upgrade rest assured to v4 ([04f971b](https://github.com/Jesse0Michael/go-rest-assured/commit/04f971bd0d5e96c971bd84a4e7f3e66ecf5e7b62)) 70 | - export Serve method to start http listener ([e3af4cc](https://github.com/Jesse0Michael/go-rest-assured/commit/e3af4ccd4e2a911afe6951365f2196ee41c02266)) 71 | - move to log/slog for logging ([ac6be67](https://github.com/Jesse0Michael/go-rest-assured/commit/ac6be679ee426bb2fd1235ecb1f401ab2a6b8bd4)) 72 | 73 | ### Tests 74 | 75 | - Serve rest assured client in tests ([5503a7d](https://github.com/Jesse0Michael/go-rest-assured/commit/5503a7dc008f9385767c47556473c90dfe922921)) 76 | 77 | ## [3.1.2](https://github.com/Jesse0Michael/go-rest-assured/compare/v3.1.1...v3.1.2) (2022-10-10) 78 | 79 | ### Continuous Integration 80 | 81 | - publish with image name ([8e22407](https://github.com/Jesse0Michael/go-rest-assured/commit/8e22407fb93d6ad3a7742bb2e03a7295cc600e4b)) 82 | 83 | ## [3.1.1](https://github.com/Jesse0Michael/go-rest-assured/compare/v3.1.0...v3.1.1) (2022-10-10) 84 | 85 | ### Continuous Integration 86 | 87 | - update docker publish ([6d8f225](https://github.com/Jesse0Michael/go-rest-assured/commit/6d8f2259b1c9778aeb742f932c81fec781790180)) 88 | 89 | # [3.1.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v3.0.2...v3.1.0) (2022-10-05) 90 | 91 | ### Documentation 92 | 93 | - update documentation ([1b04b01](https://github.com/Jesse0Michael/go-rest-assured/commit/1b04b01208f670195efce904b2528414eda162cc)) 94 | 95 | ### Features 96 | 97 | - add Assured-Method header option ([84e7754](https://github.com/Jesse0Michael/go-rest-assured/commit/84e7754716b11e05d91ef3eccd4d5be0eeb61eab)) 98 | 99 | ## [3.0.2](https://github.com/Jesse0Michael/go-rest-assured/compare/v3.0.1...v3.0.2) (2022-10-05) 100 | 101 | ### Build System 102 | 103 | - build image from static image ([4c29aa3](https://github.com/Jesse0Michael/go-rest-assured/commit/4c29aa35398873ec13afa400650aa6d7499859cd)) 104 | - update to go 1.19 ([2ed0296](https://github.com/Jesse0Michael/go-rest-assured/commit/2ed0296a95db1506af7fa78a476727368e960a0c)) 105 | 106 | ### Chores 107 | 108 | - update with linter suggestions ([7401f6f](https://github.com/Jesse0Michael/go-rest-assured/commit/7401f6f8b5f94a1bdb237ded38d004e6a8cef9ef)) 109 | 110 | ### Continuous Integration 111 | 112 | - update ci actions ([47efa38](https://github.com/Jesse0Michael/go-rest-assured/commit/47efa3886c007830def26aec4887c7b543b22292)) 113 | 114 | ### Tests 115 | 116 | - add time for async test ([30dea25](https://github.com/Jesse0Michael/go-rest-assured/commit/30dea2540a8668ad5dcc7078efd1615145bcae71)) 117 | 118 | ## [3.0.1](https://github.com/Jesse0Michael/go-rest-assured/compare/v3.0.0...v3.0.1) (2021-07-30) 119 | 120 | ### Bug Fixes 121 | 122 | - update go mod for v3 ([467b9e5](https://github.com/Jesse0Michael/go-rest-assured/commit/467b9e539cedefb0a0563e3a55a2697c44865367)) 123 | 124 | # [3.0.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.11...v3.0.0) (2020-09-03) 125 | 126 | ### Chores 127 | 128 | - update dependencies ([08ad385](https://github.com/Jesse0Michael/go-rest-assured/commit/08ad385b1017e432db395e0f6501b8d62d74e223)) 129 | - update for v3 ([d298cc7](https://github.com/Jesse0Michael/go-rest-assured/commit/d298cc76b2c0966fa7eb3572eaa0f5f9f60eefc4)) 130 | 131 | ### Features 132 | 133 | - add host option ([6490699](https://github.com/Jesse0Michael/go-rest-assured/commit/649069963e5aeaef88ef649d3eb5eecf43e3a5ce)) 134 | - add tls option ([1213d71](https://github.com/Jesse0Michael/go-rest-assured/commit/1213d718729e63536a59d886745c09d173293999)) 135 | - remove logger arg ([82f431c](https://github.com/Jesse0Michael/go-rest-assured/commit/82f431cd2b9a95a63b7f08c2cb3d0e5df83d1bc5)) 136 | - use functional options ([dedd374](https://github.com/Jesse0Michael/go-rest-assured/commit/dedd374b9b076a5194f70a4fabb2652614804c9f)) 137 | 138 | ### Tests 139 | 140 | - fix tls test ([93236a1](https://github.com/Jesse0Michael/go-rest-assured/commit/93236a163aead07bcb80c3e2f00f63f127591b91)) 141 | 142 | ### BREAKING CHANGE 143 | 144 | - remove logger option, if you want to redirect your logs in a file, use the appropriate cli commands 145 | - Use funcional options with sane defaults for configuring the assured client instead of a settings struct 146 | 147 | ## [2.0.11](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.10...v2.0.11) (2020-09-01) 148 | 149 | ### Continuous Integration 150 | 151 | - add linter ([efe26b8](https://github.com/Jesse0Michael/go-rest-assured/commit/efe26b8078affaaea85f2550aded81cadfc5df97)) 152 | 153 | ## [2.0.10](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.9...v2.0.10) (2020-08-28) 154 | 155 | ### Documentation 156 | 157 | - add response examle ([f0b00d0](https://github.com/Jesse0Michael/go-rest-assured/commit/f0b00d0357e8103efd86c7608c4efbf46198c4c4)) 158 | 159 | ## [2.0.9](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.8...v2.0.9) (2020-08-26) 160 | 161 | ### Continuous Integration 162 | 163 | - push latest ([590e4c8](https://github.com/Jesse0Michael/go-rest-assured/commit/590e4c86b9620fe8e9b4ba2fe4e26b4f70da23dc)) 164 | 165 | ## [2.0.8](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.7...v2.0.8) (2020-08-26) 166 | 167 | ### Continuous Integration 168 | 169 | - set latest tag ([104f1eb](https://github.com/Jesse0Michael/go-rest-assured/commit/104f1ebac7c0f4149cbc922d375b075f7cb8bcae)) 170 | 171 | ## [2.0.7](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.6...v2.0.7) (2020-08-26) 172 | 173 | ### Continuous Integration 174 | 175 | - separate tagging and releasing ([d0464f6](https://github.com/Jesse0Michael/go-rest-assured/commit/d0464f6cb0c518fd15193f32138c66c11d5b48d0)) 176 | 177 | ## [2.0.6](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.5...v2.0.6) (2020-08-26) 178 | 179 | ### Continuous Integration 180 | 181 | - include release reference ([3e7eae4](https://github.com/Jesse0Michael/go-rest-assured/commit/3e7eae410bc2ecfd567eb795d5c4a7433a113dcd)) 182 | 183 | ### Other 184 | 185 | - Merge branch 'master' of ssh://github.com/Jesse0Michael/go-rest-assured ([137c9fd](https://github.com/Jesse0Michael/go-rest-assured/commit/137c9fdcb69531310a5cb47d8c77e629f26bc424)) 186 | 187 | ## [2.0.5](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.4...v2.0.5) (2020-08-26) 188 | 189 | ### Continuous Integration 190 | 191 | - docker build/push ([c18d7a9](https://github.com/Jesse0Michael/go-rest-assured/commit/c18d7a9d07c9682e516a5359fce8c00a6367480e)) 192 | 193 | ## [2.0.4](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.3...v2.0.4) (2020-04-15) 194 | 195 | ### Documentation 196 | 197 | - document application preload specification ([5cd071b](https://github.com/Jesse0Michael/go-rest-assured/commit/5cd071b05d9eeb39423f878811cb7c6ed6f56423)) 198 | 199 | ## [2.0.3](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.2...v2.0.3) (2020-04-11) 200 | 201 | ### Documentation 202 | 203 | - update build badge ([8a5872b](https://github.com/Jesse0Michael/go-rest-assured/commit/8a5872b192e9e7991e116301debb454ba1c3a627)) 204 | 205 | ## [2.0.2](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.1...v2.0.2) (2020-04-11) 206 | 207 | ### Bug Fixes 208 | 209 | - remove old assured application ([355b32e](https://github.com/Jesse0Michael/go-rest-assured/commit/355b32ef74386850afc13d00297e304226e41002)) 210 | 211 | ### Other 212 | 213 | - Merge branch 'master' of ssh://github.com/Jesse0Michael/go-rest-assured ([8a82f0b](https://github.com/Jesse0Michael/go-rest-assured/commit/8a82f0bcc45657511d3bef0de0d14a78f677d5b5)) 214 | 215 | ## [2.0.1](https://github.com/Jesse0Michael/go-rest-assured/compare/v2.0.0...v2.0.1) (2020-04-11) 216 | 217 | ### Bug Fixes 218 | 219 | - separeate application and client readme ([b856342](https://github.com/Jesse0Michael/go-rest-assured/commit/b856342b3e8deac048c33cf8381447e01db33628)) 220 | 221 | # [1.1.0](https://github.com/Jesse0Michael/go-rest-assured/compare/v1.0.1...v1.1.0) (2020-04-10) 222 | 223 | ### Bug Fixes 224 | 225 | - update test package ([122d28f](https://github.com/Jesse0Michael/go-rest-assured/commit/122d28fd1ba47b808e220506479bcbbe6710ad53)) 226 | 227 | ### Features 228 | 229 | - reorgonize pkg and cmd directories ([2c00b9a](https://github.com/Jesse0Michael/go-rest-assured/commit/2c00b9af16367919513556a2328b0cacebb396ac)) 230 | 231 | ## [1.0.1](https://github.com/Jesse0Michael/go-rest-assured/compare/v1.0.0...v1.0.1) (2020-04-10) 232 | 233 | ### Chores 234 | 235 | - remove releaserc ([8d2b70b](https://github.com/Jesse0Michael/go-rest-assured/commit/8d2b70b9947f74a0be915ca272d528a7d563776b)) 236 | 237 | ### Other 238 | 239 | - Merge branch 'master' of ssh://github.com/Jesse0Michael/go-rest-assured ([804f6f1](https://github.com/Jesse0Michael/go-rest-assured/commit/804f6f1329407eebea46d36d36b6ccee89ae935a)) 240 | 241 | # 1.0.0 (2020-04-10) 242 | 243 | ### Bug Fixes 244 | 245 | - add github build and test actions ([520e814](https://github.com/Jesse0Michael/go-rest-assured/commit/520e8140ecb11683f7bc2416a107f36ece1e026e)) 246 | - fix semantic release ([80ef437](https://github.com/Jesse0Michael/go-rest-assured/commit/80ef437e8aacc0512a19ca80669e918daffcb1e2)) 247 | - try codfish's action ([08b1e86](https://github.com/Jesse0Michael/go-rest-assured/commit/08b1e8645a6867f9b02ef5fcc5f73e1428afc46c)) 248 | - try release me action ([278bd8f](https://github.com/Jesse0Michael/go-rest-assured/commit/278bd8f0a9c185c189a34d143c474672f02c5ecd)) 249 | 250 | ### Other 251 | 252 | - Fix readme typo ([2829caa](https://github.com/Jesse0Michael/go-rest-assured/commit/2829caa783abe1469cabc30f44847495586245d7)) 253 | - Fix client test. ([550c064](https://github.com/Jesse0Michael/go-rest-assured/commit/550c06415410d89c8f0f92a3be4d158eab52524f)) 254 | - Go module. ([001aabf](https://github.com/Jesse0Michael/go-rest-assured/commit/001aabf84d4a1884f1c61bf934e02de0ba6455b6)) 255 | - circle and coverage ([c3d913a](https://github.com/Jesse0Michael/go-rest-assured/commit/c3d913a29414ab1851a52589f0fc99875463d66a)) 256 | - Circle CI, my guy (#7) ([57d7b08](https://github.com/Jesse0Michael/go-rest-assured/commit/57d7b085e82ec7911437f96c2ef9e449bb26db36)), closes [#7](https://github.com/Jesse0Michael/go-rest-assured/issues/7) 257 | - write header after having configured it ([d3aa03f](https://github.com/Jesse0Michael/go-rest-assured/commit/d3aa03fea009566c0c0b416adc60a386705f2c5f)) 258 | - write header after having configured it ([c56c003](https://github.com/Jesse0Michael/go-rest-assured/commit/c56c0034e660aaf4a382438a41ac07515764f412)) 259 | - delay response ([d34f5ab](https://github.com/Jesse0Michael/go-rest-assured/commit/d34f5ab9ac8da5601c6e34878549a7a47bef7957)) 260 | - calls track query ([514778e](https://github.com/Jesse0Michael/go-rest-assured/commit/514778e6b6248b251e4d59173c2ccc4dd655f01c)) 261 | - Query in call ([b98fe2e](https://github.com/Jesse0Michael/go-rest-assured/commit/b98fe2ebc0f5a524115fbee687e31594de25f0ec)) 262 | - Merge branch 'master' into query-in-call ([a169d1b](https://github.com/Jesse0Michael/go-rest-assured/commit/a169d1b144d5d0d16980ebe7c3b24d49d59054b5)) 263 | - add query params to call ([08c107e](https://github.com/Jesse0Michael/go-rest-assured/commit/08c107eafee7add16e40700c2337616271758d09)) 264 | - custom unmarshalling ([d965d17](https://github.com/Jesse0Michael/go-rest-assured/commit/d965d1731b63803604884ab4d81d23d47c1a2c39)) 265 | - custom unmarshalling ([1b3a4b2](https://github.com/Jesse0Michael/go-rest-assured/commit/1b3a4b2603bd5715b7cd54bb56026ec309d9c400)) 266 | - tracking calls should be on be default ([841b442](https://github.com/Jesse0Michael/go-rest-assured/commit/841b4425977c6480d1ba989ed5f3e22f18af2028)) 267 | - Callbacks ([51dd7ee](https://github.com/Jesse0Michael/go-rest-assured/commit/51dd7ee9d182b231a72b1c9231f6dac81be1dc66)) 268 | - callbacks ([31f99bc](https://github.com/Jesse0Michael/go-rest-assured/commit/31f99bc5ff8104077799f244ebab36f3481818e1)) 269 | - delay from client ([d14b1fa](https://github.com/Jesse0Michael/go-rest-assured/commit/d14b1fa08e94c2fcd3b2ea4e5000811e0655d083)) 270 | - callbacks and tests ([41cea71](https://github.com/Jesse0Michael/go-rest-assured/commit/41cea716dc9acfb0f38c20df02d12e5077c3ee8d)) 271 | - callbacks ([b215ae6](https://github.com/Jesse0Michael/go-rest-assured/commit/b215ae6d2cdc81658ecfd3b8e316918f29b89ec8)) 272 | - rearrange headers ([d7cbed2](https://github.com/Jesse0Michael/go-rest-assured/commit/d7cbed29d3f6aa598fbed6509e2d78cce69325d1)) 273 | - sanitize path on client ([fec602d](https://github.com/Jesse0Michael/go-rest-assured/commit/fec602d610a4a06b8f60b2ff19c7c0c311a20652)) 274 | - support headers ([b23211b](https://github.com/Jesse0Michael/go-rest-assured/commit/b23211bdb7881623e491bd10076118f13d2f90cb)) 275 | - make tracking made calls disable able ([c8af462](https://github.com/Jesse0Michael/go-rest-assured/commit/c8af462151c0d53bc512d7537209657c49f4481e)) 276 | - preload calls ([0824afb](https://github.com/Jesse0Michael/go-rest-assured/commit/0824afbb509f19cd4895fe88fb83e74db3caf61f)) 277 | - add port and logger support for binary ([b02c3a9](https://github.com/Jesse0Michael/go-rest-assured/commit/b02c3a98bcfd4b776543b0e45e119ffbbdd64764)) 278 | - Allow mocking calls at root level ([2a38c91](https://github.com/Jesse0Michael/go-rest-assured/commit/2a38c91a33e92bc2264dd933966da80a870f4614)) 279 | - Allow mocking calls at root level ([f904cf0](https://github.com/Jesse0Michael/go-rest-assured/commit/f904cf0ca1a4d17c3ae597dc1cf6f44b4f471959)) 280 | - remove freeport ([e115920](https://github.com/Jesse0Michael/go-rest-assured/commit/e1159205631e2206d76c20d3ae4e266b0eca63ca)) 281 | - use sync.Mutex instead of channels ([c24e9cc](https://github.com/Jesse0Michael/go-rest-assured/commit/c24e9ccea35cf7f59b8b0fb8381ad7b9ba939911)) 282 | - rename files ([1c85984](https://github.com/Jesse0Michael/go-rest-assured/commit/1c85984a69f54bd32f879c429e826e6f26413546)) 283 | - why use pointers here ([3a77f19](https://github.com/Jesse0Michael/go-rest-assured/commit/3a77f19975234a155b1c78ce1181c9207b849251)) 284 | - close client ([39309a7](https://github.com/Jesse0Michael/go-rest-assured/commit/39309a785c5e74ed0e996b0518156444d231a166)) 285 | - lock assured interactions ([ddb2a6d](https://github.com/Jesse0Michael/go-rest-assured/commit/ddb2a6d18a66a65f88eb2f24a0dba18ba18c12d4)) 286 | - Configure or Default Client with Serve ([622b195](https://github.com/Jesse0Michael/go-rest-assured/commit/622b19556bb3459a9ab9b91279a9f0aba57f29c4)) 287 | - client usage testing ([81e0925](https://github.com/Jesse0Michael/go-rest-assured/commit/81e0925a1f436f7f5e12e5373bf9779474e3d8a0)) 288 | - assured client testing ([0bcebcb](https://github.com/Jesse0Michael/go-rest-assured/commit/0bcebcb8c4a7c2caca2712e46350aa95b7071555)) 289 | - client up ([fd4e504](https://github.com/Jesse0Michael/go-rest-assured/commit/fd4e504a85c8acea6738fce14bdb4ebfdaaee6f2)) 290 | - change then to verify, and use free port ([4fe7037](https://github.com/Jesse0Michael/go-rest-assured/commit/4fe70370520dd498577e35efe5a614837ded2e32)) 291 | - license ([99921e8](https://github.com/Jesse0Michael/go-rest-assured/commit/99921e8453f517262a10fdb0da7e299a61df5187)) 292 | - go fmt ([0bb6436](https://github.com/Jesse0Michael/go-rest-assured/commit/0bb6436237ec2c907902220db35da45cba6cbccc)) 293 | - move to one package ([15b8587](https://github.com/Jesse0Michael/go-rest-assured/commit/15b85875ff1077eba96eb8af5bd2ed8675348b95)) 294 | - populate readme ([9c2d0ce](https://github.com/Jesse0Michael/go-rest-assured/commit/9c2d0ce64db27f8dd70fa578374824e09bbb178d)) 295 | - fix binding and endpoints ([7e3bf63](https://github.com/Jesse0Michael/go-rest-assured/commit/7e3bf63df07b1557e1c3cd82aadfc88c92cc6b7a)) 296 | - wrap endpoints, test bindings, assure calls ([1e20981](https://github.com/Jesse0Michael/go-rest-assured/commit/1e20981fb494b6134020bc63694de22717cf47b0)) 297 | - rest assured endpoints with tests ([02786fa](https://github.com/Jesse0Michael/go-rest-assured/commit/02786fae69287d2d502b821dd495306486fe7b0a)) 298 | - assured bindings and endpoints ([9b13b09](https://github.com/Jesse0Michael/go-rest-assured/commit/9b13b091ead860dccec7da61693ce32b1259323e)) 299 | - go-rest-assured ([2022320](https://github.com/Jesse0Michael/go-rest-assured/commit/20223202b5097da4536544fc1ee959531dc6b152)) 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Jesse Michael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | COVERAGEDIR = .coverage 3 | GITSHA = $(shell git rev-parse HEAD) 4 | LDFLAGS = -ldflags '-X main.gitSHA=$(GITSHA)' 5 | 6 | all: build test cover 7 | dependencies: 8 | go mod download 9 | build: 10 | if [ ! -d bin ]; then mkdir bin; fi 11 | go build $(LDFLAGS) -v -o bin/go-rest-assured ./cmd/go-assured 12 | docker-build: 13 | docker build -f ./build/Dockerfile -t go-rest-assured:$(GITSHA) . 14 | fmt: 15 | go mod tidy 16 | gofmt -w -l -s . 17 | golangci-lint run ./... 18 | assert-no-diff: 19 | test -z "$(shell git status --porcelain)" 20 | test: fmt 21 | if [ ! -d $(COVERAGEDIR) ]; then mkdir $(COVERAGEDIR); fi 22 | go test -v ./pkg/... -cover -coverprofile=$(COVERAGEDIR)/assured.coverprofile 23 | cover: 24 | if [ ! -d $(COVERAGEDIR) ]; then mkdir $(COVERAGEDIR); fi 25 | go tool cover -html=$(COVERAGEDIR)/assured.coverprofile 26 | clean: 27 | go clean 28 | rm -f bin/go-rest-assured 29 | rm -rf $(COVERAGEDIR) 30 | rm -rf vendor/ 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GO REST ASSURED 2 | 3 | [![Build](https://github.com/Jesse0Michael/go-rest-assured/workflows/Build/badge.svg)](https://github.com/Jesse0Michael/go-rest-assured/actions?query=branch%3Amain) [![Coverage Status](https://coveralls.io/repos/github/Jesse0Michael/go-rest-assured/badge.svg?branch=main)](https://coveralls.io/github/Jesse0Michael/go-rest-assured?branch=main) 4 | 5 | Go Rest Assured is a small service written in GO intended to be used to mock out REST API applications for testing. The concept is based on the [Rest Assured](http://rest-assured.io/) service written in Java and [other languages](https://github.com/artemave/REST-assured) 6 | 7 | Go-Rest-Assured keeps track of the Assured Calls you have stubbed out and the Calls that have been made against the service with the following fields: 8 | 9 | - Path 10 | - StatusCode 11 | - Method 12 | - Response 13 | - Headers 14 | - Query 15 | - Delay 16 | - Callbacks 17 | 18 | Set these fields as a _Given_ call through the client or a HTTP request to the service directly and they will be returned from the Go Rest Assured API when you hit the _When_ endpoint. The Calls you stub out are uniquely mapped with an identity of their Method and Path. If you stub multiple calls to the same Method and Path, the responses will cycle through your stubs based on the order they were created. 19 | 20 | If loading callbacks from a JSON file, the call [unmarshaller](pkg/assured/call.go) will attempt to read the resource field as a relative file, or else a quoted string, or else just a byte slice. 21 | 22 | To understand how rest assured is working behind the scenes, or to use rest assured as a standalone application you can run from a command line and use anywhere, or how to serve HTTPS traffic from rest assured, read the application [README](cmd/go-assured/README.md) 23 | 24 | ### Client 25 | 26 | ```go 27 | import ("github.com/jesse0michael/go-rest-assured/v4/pkg/assured") 28 | 29 | // Create and Serve a new Assured Client 30 | client := assured.NewClientServe() 31 | defer client.Close() 32 | ``` 33 | 34 | ## Stubbing 35 | 36 | ```go 37 | call := assured.Call{ 38 | Path: "test/assured", 39 | StatusCode: 201, 40 | Method: "GET", 41 | Delay: 2, 42 | } 43 | // Stub out an assured call 44 | client.Given(call) 45 | ``` 46 | 47 | _If your stubbed endpoint needs to return a different call on a subsequent request, then try stubbing that Method/Path again. The first time you intercept that endpoint the first call will be returned and then moved to the end of the list._ 48 | 49 | ## Intercepting 50 | 51 | To use your assured calls hit the following endpoint with the Method/Path that was used to stub the call 52 | 53 | ```go 54 | // Get the URL of the client ex: 'http://localhost:11011/when' 55 | testServer := client.URL() 56 | ``` 57 | 58 | Go-Rest-Assured will return `404 NotFound` error response when a matching stub isn't found 59 | 60 | As requests come in, the will be stored 61 | 62 | ## Callbacks 63 | To have the mock server programmatically make a callback to a specified target, use the Callback field 64 | 65 | ```go 66 | call := assured.Call{ 67 | Path: "test/assured", 68 | StatusCode: 201, 69 | Method: "POST", 70 | Response: []byte(`{"holler_back":true}`), 71 | Callbacks: []assured.Callback{ 72 | assured.Callback{ 73 | Method: "POST", 74 | Target: "http://localhost:8080/hit/me", 75 | Response: []byte(`holla!!`), 76 | }, 77 | }, 78 | } 79 | // Stub out an assured call with callbacks 80 | client.Given(call) 81 | ``` 82 | 83 | _You cannot clear out an individual callback when using the assured.Client, but you can `ClearAll()`_ 84 | 85 | ## Verifying 86 | 87 | To verify the calls made against your go-rest-assured service, use the Verify function. 88 | 89 | This function returns a list of calls made against the matching Method/Path 90 | 91 | ```go 92 | // Get a []*assured.Call for a Method and Path 93 | calls := client.Verify("GET", "test/assured") 94 | ``` 95 | 96 | ## Clearing 97 | 98 | To clear out the stubbed and made calls for a specific Method/Path, use Clear(method, path) 99 | 100 | To clear out all stubbed calls on the server, use ClearAll() 101 | 102 | ```go 103 | // Clears calls for a Method and Path 104 | client.Clear("GET", "test/assured") 105 | 106 | // Clears all calls 107 | client.ClearAll() 108 | ``` 109 | -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS build 2 | 3 | # Copy project files 4 | WORKDIR /go/src 5 | COPY go.mod . 6 | COPY go.sum . 7 | RUN mkdir /dir 8 | 9 | # Fetch dependencies 10 | RUN go mod download 11 | COPY . . 12 | 13 | # Fetch dependencies (go mod) 14 | RUN go mod download 15 | RUN go mod verify 16 | 17 | # Build GO binary 18 | ENV CGO_ENABLED=0 19 | RUN go build -o go-rest-assured ./cmd/go-assured 20 | 21 | # Build final application 22 | FROM gcr.io/distroless/base:debug AS app 23 | 24 | LABEL org.opencontainers.image.source "https://github.com/jesse0michael/go-rest-assured" 25 | LABEL org.opencontainers.image.description "Docker image for the GO Rest Assured client to mock and validate your calls to REST API's" 26 | 27 | COPY --from=build /go/src/go-rest-assured ./ 28 | 29 | ENTRYPOINT ["./go-rest-assured"] 30 | -------------------------------------------------------------------------------- /cmd/go-assured/README.md: -------------------------------------------------------------------------------- 1 | # GO REST ASSURED - CMD 2 | 3 | Go Rest Assured can be used from the command line to spin up a mock rest api. The rest assured application will take in some arguments to configure configure a server to mock rest calls. 4 | 5 | ## Running 6 | 7 | 1. `go get github.com/jesse0michael/go-rest-assured/cmd/go-assured` 8 | 9 | ``` 10 | Usage of go-assured: 11 | -host string 12 | a host to use in the client's url. (default "localhost") 13 | -port int 14 | a port to listen on. default automatically assigns a port. 15 | -preload string 16 | a file to parse preloaded calls from. 17 | -tlsCert string 18 | location of tls cert for serving https traffic. tlsKey also required, if specified. 19 | -tlsKey string 20 | location of tls key for serving https traffic. tlsCert also required, if specified 21 | -track 22 | a flag to enable the storing of calls made to the service. (default true) 23 | ``` 24 | 25 | To load in a default set of stubbed endpoints from a file, follow the [Preload API Reference](preload_reference.md) guide. 26 | 27 | You can specify a TLS cert/key to mock out HTTPS traffic using [mkcert](https://github.com/FiloSottile/mkcert) self signed certs and mock HTTPS traffic. 28 | 29 | ## Stubbing 30 | 31 | To stub out an assured call hit the following endpoint 32 | `/given/{path:.*}` 33 | 34 | The HTTP Method you use will be stored in the Assured Call unless you specify a `"Assured-Method": "[a-zA-Z]+"` HTTP Header. 35 | 36 | The Request Body, if present, will be stored in the Assured Call 37 | 38 | The stored Status Code will be `200 OK` unless you specify a `"Assured-Status": "[0-9]+"` HTTP Header 39 | 40 | You can also set a response delay with the HTTP Header `Assured-Delay` with a number of seconds 41 | 42 | 43 | _If your stubbed endpoint needs to return a different call on a subsequent request, then try stubbing that Method/Path again. The first time you intercept that endpoint the first call will be returned and then moved to the end of the list._ 44 | 45 | ## Intercepting 46 | 47 | To use your assured calls hit the following endpoint with the Method/Path that was used to stub the call `/when/{path:.*}` 48 | 49 | Go-Rest-Assured will return `404 NotFound` error response when a matching stub isn't found 50 | 51 | As requests come in, the will be stored 52 | 53 | ## Callbacks 54 | 55 | To include callbacks from Go-Rest-Assured when a stubbed endpoint is hit, create them by hitting the endpoint `/callbacks` 56 | To create a callbacks you must include the HTTP header `Assured-Callback-Target` with the specified endpoint you want your callbacks to be sent to 57 | You must also include the HTTP header `Assured-Callback-Key` with a key with the call to the `/callbacks` endpoint as well as the `/given/{path:.*}` endpoint that for the stubbed call you want the callback to be associated with 58 | You can also set a callback delay with the HTTP Header `Assured-Callback-Delay` with a number of seconds 59 | 60 | ## Verifying 61 | 62 | To verify the calls made against your go-rest-assured service, use the endpoint `/verify/{path:.*}` 63 | 64 | This endpoint returns a list of assured calls made against the matching Method/Path 65 | 66 | ``` 67 | [ 68 | { 69 | "path": "test/assured", 70 | "method": "GET", 71 | "status_code": 200, 72 | "delay": 0, 73 | "response": "eyJhc3N1cmVkIjogdHJ1ZX0=", 74 | "headers": { 75 | "Content-Length": "17", 76 | "User-Agent": "Go-http-client/1.1", 77 | }, 78 | "query": { 79 | "assured": "max" 80 | } 81 | }, 82 | { 83 | "path": "test/assured", 84 | "method": "GET", 85 | "status_code": 409, 86 | "delay": 0, 87 | "response": "ZXJyb3I=", 88 | "headers": { 89 | "Content-Length": "5", 90 | "User-Agent": "Go-http-client/1.1", 91 | } 92 | } 93 | ] 94 | 95 | ``` 96 | 97 | ## Clearing 98 | 99 | To clear out the stubbed and made calls for a specific Method/Path, use the endpoint DELETE `/clear/{path:.*}` 100 | _Including the HTTP Header `Assured-Callback-Key` will clear all callbacks associated with that key (independent of path)_ 101 | 102 | To clear out all stubbed calls on the server, use the endpoint `/clear` 103 | -------------------------------------------------------------------------------- /cmd/go-assured/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "log/slog" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | 13 | "github.com/jesse0michael/go-rest-assured/v4/pkg/assured" 14 | ) 15 | 16 | // Preload is the expected format for preloading assured endpoints through the go rest assured application 17 | type Preload struct { 18 | Calls []assured.Call `json:"calls"` 19 | } 20 | 21 | func main() { 22 | ctx, cancel := context.WithCancelCause(context.Background()) 23 | sig := make(chan os.Signal, 1) 24 | signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT) 25 | go func() { 26 | cancel(fmt.Errorf("%s", <-sig)) 27 | }() 28 | 29 | port := flag.Int("port", 0, "a port to listen on. default automatically assigns a port.") 30 | preload := flag.String("preload", "", "a file to parse preloaded calls from.") 31 | trackMade := flag.Bool("track", true, "a flag to enable the storing of calls made to the service.") 32 | host := flag.String("host", "localhost", "a host to use in the client's url.") 33 | tlsCert := flag.String("tlsCert", "", "location of tls cert for serving https traffic. tlsKey also required, if specified.") 34 | tlsKey := flag.String("tlsKey", "", "location of tls key for serving https traffic. tlsCert also required, if specified") 35 | 36 | flag.Parse() 37 | 38 | client := assured.NewClient( 39 | assured.WithPort(*port), 40 | assured.WithCallTracking(*trackMade), 41 | assured.WithHost(*host), 42 | assured.WithTLS(*tlsCert, *tlsKey)) 43 | 44 | go func() { 45 | slog.With("port", client.Port).Info("starting go rest assured client") 46 | if err := client.Serve(); err != nil { 47 | slog.With("error", err).Info("rest assured server stopped serving") 48 | } 49 | }() 50 | 51 | // If preload file specified, parse the file and load all calls into the assured client 52 | if *preload != "" { 53 | b, err := os.ReadFile(*preload) 54 | if err != nil { 55 | slog.With("error", err).Info("failed to read preload file") 56 | cancel(err) 57 | } 58 | var preload Preload 59 | // TODO response won't unmarshal string to []byte 60 | if err := json.Unmarshal(b, &preload); err != nil { 61 | slog.With("error", err).Info("failed to unmarshal preload file") 62 | cancel(err) 63 | } 64 | if err = client.Given(preload.Calls...); err != nil { 65 | slog.With("error", err).Info("failed to set given preload file calls") 66 | cancel(err) 67 | } 68 | } 69 | 70 | <-ctx.Done() 71 | client.Close() 72 | slog.Info("exiting go rest assured") 73 | } 74 | -------------------------------------------------------------------------------- /cmd/go-assured/preload_reference.md: -------------------------------------------------------------------------------- 1 | ## Preload Rest Assured Endpoints 2 | 3 | To stub rest assured endpoints with a json file, pass a JSON file to the `-preload` argument that follows this specification: 4 | 5 | ## Example 6 | 7 | ```json 8 | { 9 | "calls": [ 10 | { 11 | "path": "test/assured", 12 | "method": "GET", 13 | "status_code": 201, 14 | "delay": 0, 15 | "response": "testdata/assured.json", 16 | "headers": { 17 | "Content-Length": "17", 18 | "User-Agent": "Go-http-client/1.1", 19 | "Accept-Encoding": "gzip" 20 | } 21 | }, 22 | { 23 | "path": "test/assured", 24 | "method": "GET", 25 | "status_code": 200, 26 | "delay": 0, 27 | "response": "testdata/image.jpg", 28 | "headers": { 29 | "Content-Length": "56000", 30 | "Content-Type": "image/jpeg", 31 | "User-Agent": "Go-http-client/1.1" 32 | } 33 | }, 34 | { 35 | "path": "teapot/assured", 36 | "method": "POST", 37 | "status_code": 418, 38 | "delay": 2, 39 | "headers": { 40 | "Content-Length": "0", 41 | "User-Agent": "Go-http-client/1.1", 42 | "Accept-Encoding": "gzip" 43 | }, 44 | "callbacks": [ 45 | { 46 | "target": "http://localhost:9000/when/capture", 47 | "method": "PUT", 48 | "delay": 5, 49 | "response": "Therefore do not worry about tomorrow, for tomorrow will worry about itself. Each day has enough trouble of its own." 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | ``` 56 | 57 | ### calls 58 | 59 | **[object array]** The rest assured calls loaded into the go rest assured application 60 | 61 | ```json 62 | { 63 | "calls": [ 64 | ... 65 | ] 66 | } 67 | ``` 68 | 69 | ### calls[x].path 70 | **[string]** The http path to the endpoints. 71 | 72 | ```json 73 | { 74 | "path": "test/assured", 75 | ... 76 | } 77 | ``` 78 | *When call this path, to receive the stubbed response you need to include the `/when/` path prefix. e.g. `http://localhost:8888/when/test/assured`* 79 | 80 | ### calls[x].method 81 | **[string]** The http method to the endpoints. Defaults to "GET". 82 | 83 | ```json 84 | { 85 | ... 86 | "method": "POST", 87 | ... 88 | } 89 | ``` 90 | 91 | ### calls[x].status_code 92 | **[int]** The http status code to respond with. Defaults to 200 OK. 93 | 94 | ```json 95 | { 96 | ... 97 | "status_code": 201, 98 | ... 99 | } 100 | ``` 101 | 102 | ### calls[x].response 103 | **[string]** The http response body to respond with using a custom and complex JSON unmarshall function. Unmarshalling will first check if the data is a local file path that can be read. Else it will check if the data is stringified JSON and un-stringify the data to use. Else it will just use the []byte. Optional. 104 | 105 | ```json 106 | { 107 | ... 108 | "response": "responses/success.json", 109 | ... 110 | }, 111 | { 112 | ... 113 | "response": "{\"happy\": true}", 114 | ... 115 | }, 116 | { 117 | ... 118 | "response": "string cheese", 119 | ... 120 | } 121 | ``` 122 | 123 | ### calls[x].headers 124 | **[object]** The http headers to include with the response. Keys and values must be strings. 125 | 126 | ```json 127 | { 128 | ... 129 | "headers": { 130 | "Content-Length": "17", 131 | "User-Agent": "Go-http-client/1.1" 132 | }, 133 | ... 134 | } 135 | ``` 136 | 137 | ### calls[x].callbacks 138 | **[object array]** Specified callbacks to be made by the go rest assured application when an endpoint is hit with specified parameters. Optional. 139 | 140 | ```json 141 | { 142 | ... 143 | "callbacks": [ 144 | ... 145 | ] 146 | } 147 | ``` 148 | 149 | ### calls[x].callbacks[x].target 150 | **[string]** The http target too hit with the callback. Required 151 | 152 | ```json 153 | { 154 | "target": "http://localhost:9000/when/capture", 155 | ... 156 | } 157 | ``` 158 | 159 | ### calls[x].callbacks[x].method 160 | **[string]** The http method to use with the callback. Defaults to "GET". 161 | 162 | ```json 163 | { 164 | "method": "PUT", 165 | ... 166 | } 167 | ``` 168 | 169 | ### calls[x].callbacks[x].response 170 | **[string]** The http response body to respond with in the callback. uses the same custom and complex JSON unmarshall function as the endpoint's response. Unmarshalling will first check if the data is a local file path that can be read. Else it will check if the data is stringified JSON and un-stringify the data to use. Else it will just use the []byte. Optional. 171 | 172 | ```json 173 | { 174 | ... 175 | "response": "responses/success.json", 176 | ... 177 | }, 178 | { 179 | ... 180 | "response": "{\"happy\": true}", 181 | ... 182 | }, 183 | { 184 | ... 185 | "response": "string cheese", 186 | ... 187 | } 188 | ``` 189 | 190 | ### calls[x].callbacks[x].headers 191 | **[object]** The http headers to include with the callback. Keys and values must be strings. 192 | 193 | ```json 194 | { 195 | ... 196 | "headers": { 197 | "Content-Length": "17", 198 | "User-Agent": "Go-http-client/1.1" 199 | }, 200 | ... 201 | } 202 | ``` 203 | 204 | ### calls[x].callbacks[x].delay 205 | **[int]** A synthetic delay, in seconds, to delay the callback from triggering. Optional. 206 | 207 | ```json 208 | { 209 | ... 210 | "delay": 2 211 | } 212 | ``` 213 | 214 | 215 | --- 216 | 217 | Follow the go rest assured application [README.md](README.md) for instructions on how to interact with your stub server 218 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jesse0michael/go-rest-assured/v4 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/google/uuid v1.3.1 7 | github.com/stretchr/testify v1.8.4 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.1.0 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 4 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 13 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /pkg/assured/bindings_test.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // go-rest-assured test vars 8 | // var ( 9 | // verbs = []string{ 10 | // http.MethodGet, 11 | // http.MethodHead, 12 | // http.MethodPost, 13 | // http.MethodPut, 14 | // http.MethodPatch, 15 | // http.MethodDelete, 16 | // http.MethodConnect, 17 | // http.MethodOptions, 18 | // } 19 | // fullAssuredCalls = &CallStore{ 20 | // data: map[string][]*Call{ 21 | // "GET:test/assured": {testCall1(), testCall2()}, 22 | // "POST:teapot/assured": {testCall3()}, 23 | // }, 24 | // } 25 | // ) 26 | 27 | func testCall1() *Call { 28 | return &Call{ 29 | Path: "test/assured", 30 | Method: "GET", 31 | StatusCode: http.StatusOK, 32 | Response: []byte(`{"assured": true}`), 33 | Headers: map[string]string{"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 34 | Query: map[string]string{"assured": "max"}, 35 | } 36 | } 37 | 38 | func testCall2() *Call { 39 | return &Call{ 40 | Path: "test/assured", 41 | Method: "GET", 42 | StatusCode: http.StatusConflict, 43 | Response: []byte("error"), 44 | Headers: map[string]string{"Content-Length": "5", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 45 | } 46 | } 47 | 48 | func testCall3() *Call { 49 | return &Call{ 50 | Path: "teapot/assured", 51 | Method: "POST", 52 | StatusCode: http.StatusTeapot, 53 | Headers: map[string]string{"Content-Length": "0", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 54 | } 55 | } 56 | 57 | // func testCallback() *Call { 58 | // return &Call{ 59 | // Response: []byte(`{"done": true}`), 60 | // Method: "POST", 61 | // Headers: map[string]string{"Assured-Callback-Key": "call-key", "Assured-Callback-Target": "http://faketarget.com/"}, 62 | // } 63 | // } 64 | -------------------------------------------------------------------------------- /pkg/assured/call.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | ) 10 | 11 | // Call is a structure containing a request that is stubbed or made 12 | type Call struct { 13 | Path string `json:"path"` 14 | Method string `json:"method"` 15 | StatusCode int `json:"status_code"` 16 | Delay int `json:"delay"` 17 | Headers map[string]string `json:"headers"` 18 | Query map[string]string `json:"query,omitempty"` 19 | Response CallResponse `json:"response,omitempty"` 20 | Callbacks []Callback `json:"callbacks,omitempty"` 21 | } 22 | 23 | // ID is used as a key when managing stubbed and made calls 24 | func (c Call) ID() string { 25 | return fmt.Sprintf("%s:%s", c.Method, c.Path) 26 | } 27 | 28 | // String converts a Call's Response into a string 29 | func (c Call) String() string { 30 | rawString := string(c.Response) 31 | 32 | // TODO: implement string replacements for special cases 33 | return rawString 34 | } 35 | 36 | // CallResponse allows control over the Call's Response encoding 37 | type CallResponse []byte 38 | 39 | // UnmarshalJSON is a custom implementation for JSON Unmarshalling for the CallResponse 40 | // Unmarshalling will first check if the data is a local filepath that can be read 41 | // Else it will check if the data is stringified JSON and un-stringify the data to use 42 | // or Else it will just use the []byte 43 | func (response *CallResponse) UnmarshalJSON(data []byte) error { 44 | unmarshaled := []byte{} 45 | if err := json.Unmarshal(data, &unmarshaled); err != nil { 46 | // The data is a []byte, so use it 47 | unmarshaled = data 48 | } 49 | 50 | if s, err := strconv.Unquote(string(unmarshaled)); err == nil { 51 | absPath, _ := filepath.Abs(s) 52 | if _, err := os.Stat(absPath); err == nil { 53 | // The data is a path that exists, therefore we will read the file 54 | if file, err := os.ReadFile(absPath); err == nil { 55 | *response = file 56 | return nil 57 | } 58 | } 59 | // The data is stringified JSON, therefore we eill use the unquoted JSON 60 | *response = []byte(s) 61 | return nil 62 | } 63 | 64 | *response = unmarshaled 65 | return nil 66 | } 67 | 68 | // Callback is a structure containing a callback that is stubbed 69 | type Callback struct { 70 | Target string `json:"target"` 71 | Method string `json:"method"` 72 | Delay int `json:"delay,omitempty"` 73 | Headers map[string]string `json:"headers"` 74 | Response CallResponse `json:"response,omitempty"` 75 | } 76 | -------------------------------------------------------------------------------- /pkg/assured/call_store.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type CallStore struct { 8 | data map[string][]*Call 9 | sync.Mutex 10 | } 11 | 12 | func NewCallStore() *CallStore { 13 | return &CallStore{data: map[string][]*Call{}} 14 | } 15 | 16 | func (c *CallStore) Add(call *Call) { 17 | c.Lock() 18 | c.data[call.ID()] = append(c.data[call.ID()], call) 19 | c.Unlock() 20 | } 21 | 22 | func (c *CallStore) AddAt(key string, call *Call) { 23 | c.Lock() 24 | c.data[key] = append(c.data[key], call) 25 | c.Unlock() 26 | } 27 | 28 | func (c *CallStore) Rotate(call *Call) { 29 | c.Lock() 30 | c.data[call.ID()] = append(c.data[call.ID()][1:], call) 31 | c.Unlock() 32 | } 33 | 34 | func (c *CallStore) Get(key string) []*Call { 35 | c.Lock() 36 | calls := c.data[key] 37 | c.Unlock() 38 | return calls 39 | } 40 | 41 | func (c *CallStore) Clear(key string) { 42 | c.Lock() 43 | delete(c.data, key) 44 | c.Unlock() 45 | } 46 | 47 | func (c *CallStore) ClearAll() { 48 | c.Lock() 49 | c.data = map[string][]*Call{} 50 | c.Unlock() 51 | } 52 | -------------------------------------------------------------------------------- /pkg/assured/call_test.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCallID(t *testing.T) { 11 | call := Call{ 12 | Path: "/given/test/assured", 13 | Method: "GET", 14 | } 15 | 16 | require.Equal(t, "GET:/given/test/assured", call.ID()) 17 | } 18 | 19 | func TestCallIDNil(t *testing.T) { 20 | call := Call{} 21 | 22 | require.Equal(t, ":", call.ID()) 23 | } 24 | 25 | func TestCallString(t *testing.T) { 26 | call := Call{ 27 | Response: []byte("GO assured is one way to GO"), 28 | } 29 | 30 | require.Equal(t, "GO assured is one way to GO", call.String()) 31 | } 32 | 33 | func TestCallStringNil(t *testing.T) { 34 | call := Call{} 35 | 36 | require.Equal(t, "", call.String()) 37 | } 38 | 39 | func TestCallUnmarshalNoResponse(t *testing.T) { 40 | raw := `{ 41 | "path": "teapot/assured", 42 | "method": "POST", 43 | "status_code": 418, 44 | "headers": {"Content-Length": "0", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"} 45 | }` 46 | 47 | call := Call{} 48 | err := json.Unmarshal([]byte(raw), &call) 49 | require.NoError(t, err) 50 | require.Equal(t, *testCall3(), call) 51 | } 52 | 53 | func TestCallUnmarshalString(t *testing.T) { 54 | raw := `{ 55 | "path": "test/assured", 56 | "method": "GET", 57 | "status_code": 409, 58 | "headers": {"Content-Length": "5", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 59 | "response": "error" 60 | }` 61 | 62 | call := Call{} 63 | err := json.Unmarshal([]byte(raw), &call) 64 | require.NoError(t, err) 65 | require.Equal(t, *testCall2(), call) 66 | } 67 | 68 | func TestCallUnmarshalJSON(t *testing.T) { 69 | raw := `{ 70 | "path": "test/assured", 71 | "method": "GET", 72 | "status_code": 200, 73 | "headers": {"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 74 | "query": {"assured": "max"}, 75 | "response": "{\"assured\": true}" 76 | }` 77 | 78 | call := Call{} 79 | err := json.Unmarshal([]byte(raw), &call) 80 | require.NoError(t, err) 81 | require.Equal(t, *testCall1(), call) 82 | } 83 | 84 | func TestCallUnmarshalBytes(t *testing.T) { 85 | raw := `{ 86 | "path": "test/assured", 87 | "method": "GET", 88 | "status_code": 200, 89 | "headers": {"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 90 | "query": {"assured": "max"}, 91 | "response": "eyJhc3N1cmVkIjogdHJ1ZX0=" 92 | }` 93 | 94 | call := Call{} 95 | err := json.Unmarshal([]byte(raw), &call) 96 | require.NoError(t, err) 97 | require.Equal(t, *testCall1(), call) 98 | } 99 | 100 | func TestCallUnmarshalFile(t *testing.T) { 101 | raw := `{ 102 | "path": "test/assured", 103 | "method": "GET", 104 | "status_code": 200, 105 | "headers": {"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 106 | "query": {"assured": "max"}, 107 | "response": "testdata/assured.json" 108 | }` 109 | 110 | call := Call{} 111 | err := json.Unmarshal([]byte(raw), &call) 112 | require.NoError(t, err) 113 | require.Equal(t, *testCall1(), call) 114 | } 115 | 116 | func TestCallUnmarshalCallbacks(t *testing.T) { 117 | raw := `{ 118 | "path": "test/assured", 119 | "method": "GET", 120 | "status_code": 200, 121 | "headers": {"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 122 | "query": {"assured": "max"}, 123 | "response": "testdata/assured.json", 124 | "callbacks": [ 125 | { 126 | "target": "http://faketarget.com/", 127 | "method": "POST", 128 | "response": "{\"done\": true}", 129 | "headers": {"Assured-Callback-Key": "call-key", "Assured-Callback-Target": "http://faketarget.com/"}} 130 | ] 131 | }` 132 | expected := *testCall1() 133 | expected.Callbacks = []Callback{ 134 | { 135 | Target: "http://faketarget.com/", 136 | Response: []byte(`{"done": true}`), 137 | Method: "POST", 138 | Headers: map[string]string{"Assured-Callback-Key": "call-key", "Assured-Callback-Target": "http://faketarget.com/"}, 139 | }, 140 | } 141 | 142 | call := Call{} 143 | err := json.Unmarshal([]byte(raw), &call) 144 | require.NoError(t, err) 145 | require.Equal(t, expected, call) 146 | } 147 | -------------------------------------------------------------------------------- /pkg/assured/client.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/google/uuid" 13 | ) 14 | 15 | const ( 16 | AssuredStatus = "Assured-Status" 17 | AssuredMethod = "Assured-Method" 18 | AssuredDelay = "Assured-Delay" 19 | AssuredCallbackKey = "Assured-Callback-Key" 20 | AssuredCallbackTarget = "Assured-Callback-Target" 21 | AssuredCallbackDelay = "Assured-Callback-Delay" 22 | ) 23 | 24 | // Client 25 | type Client struct { 26 | Options 27 | listener net.Listener 28 | router *http.ServeMux 29 | assuredCalls *CallStore 30 | madeCalls *CallStore 31 | callbackCalls *CallStore 32 | } 33 | 34 | // NewClient creates a new go-rest-assured client 35 | func NewClient(opts ...Option) *Client { 36 | c := Client{ 37 | Options: DefaultOptions, 38 | assuredCalls: NewCallStore(), 39 | madeCalls: NewCallStore(), 40 | callbackCalls: NewCallStore(), 41 | } 42 | c.Options.applyOptions(opts...) 43 | c.router = routes(c.logger, c.assuredCalls, c.madeCalls, c.callbackCalls, c.httpClient, c.trackMadeCalls) 44 | 45 | var err error 46 | c.listener, err = net.Listen("tcp", fmt.Sprintf(":%d", c.Options.Port)) 47 | if err != nil { 48 | c.logger.With("error", err, "port", c.Options.Port).Error("unable to create http listener") 49 | } else { 50 | c.Options.Port = c.listener.Addr().(*net.TCPAddr).Port 51 | } 52 | 53 | return &c 54 | } 55 | 56 | // NewClient creates a new go-rest-assured client and starts serving traffic 57 | func NewClientServe(opts ...Option) *Client { 58 | client := NewClient(opts...) 59 | go func() { 60 | _ = client.Serve() 61 | }() 62 | 63 | return client 64 | } 65 | 66 | // Serve starts the Rest Assured client to begin listening on the application endpoints 67 | func (c *Client) Serve() error { 68 | if c.listener == nil { 69 | return fmt.Errorf("invalid client") 70 | } 71 | 72 | if c.tlsCertFile != "" && c.tlsKeyFile != "" { 73 | return http.ServeTLS(c.listener, c.router, c.tlsCertFile, c.tlsKeyFile) 74 | } else { 75 | return http.Serve(c.listener, c.router) 76 | } 77 | } 78 | 79 | // url returns the url to used by the client internally 80 | func (c *Client) url() string { 81 | schema := "http" 82 | if c.tlsCertFile != "" && c.tlsKeyFile != "" { 83 | schema = "https" 84 | } 85 | return fmt.Sprintf("%s://%s:%d", schema, c.host, c.Port) 86 | } 87 | 88 | // URL returns the url to use to test you stubbed endpoints 89 | func (c *Client) URL() string { 90 | return fmt.Sprintf("%s/when", c.url()) 91 | } 92 | 93 | // Close is used to close the running service 94 | func (c *Client) Close() error { 95 | return c.listener.Close() 96 | } 97 | 98 | // Given stubs assured Call(s) 99 | func (c *Client) Given(calls ...Call) error { 100 | for _, call := range calls { 101 | // Default method to GET 102 | if call.Method == "" { 103 | call.Method = http.MethodGet 104 | } 105 | 106 | // Sanitize Path 107 | call.Path = strings.Trim(call.Path, "/") 108 | 109 | req, err := http.NewRequest(call.Method, fmt.Sprintf("%s/given/%s", c.url(), call.Path), bytes.NewReader(call.Response)) 110 | if err != nil { 111 | return err 112 | } 113 | if call.StatusCode != 0 { 114 | req.Header.Set(AssuredStatus, strconv.Itoa(call.StatusCode)) 115 | } 116 | if call.Delay > 0 { 117 | req.Header.Set(AssuredDelay, strconv.Itoa(call.Delay)) 118 | } 119 | for key, value := range call.Headers { 120 | req.Header.Set(key, value) 121 | } 122 | 123 | // Create callbacks 124 | callbacks := make([]*http.Request, len(call.Callbacks)) 125 | callbackKey := uuid.NewString() 126 | for i, callback := range call.Callbacks { 127 | if callback.Target == "" { 128 | return fmt.Errorf("cannot stub callback without target") 129 | } 130 | callbackReq, err := http.NewRequest(callback.Method, fmt.Sprintf("%s/callback", c.url()), bytes.NewReader(callback.Response)) 131 | if err != nil { 132 | return err 133 | } 134 | callbackReq.Header.Set(AssuredCallbackTarget, callback.Target) 135 | callbackReq.Header.Set(AssuredCallbackKey, callbackKey) 136 | if callback.Delay > 0 { 137 | callbackReq.Header.Set(AssuredCallbackDelay, strconv.Itoa(callback.Delay)) 138 | } 139 | for key, value := range callback.Headers { 140 | callbackReq.Header.Set(key, value) 141 | } 142 | callbacks[i] = callbackReq 143 | } 144 | if len(callbacks) > 0 { 145 | req.Header.Set(AssuredCallbackKey, callbackKey) 146 | } 147 | 148 | if _, err = c.httpClient.Do(req); err != nil { 149 | return err 150 | } 151 | for _, cReq := range callbacks { 152 | if _, err = c.httpClient.Do(cReq); err != nil { 153 | return err 154 | } 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | // Verify returns all of the calls made against a stubbed method and path 161 | func (c *Client) Verify(method, path string) ([]Call, error) { 162 | req, err := http.NewRequest(method, fmt.Sprintf("%s/verify/%s", c.url(), path), nil) 163 | if err != nil { 164 | return nil, err 165 | } 166 | resp, err := c.httpClient.Do(req) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | if resp.StatusCode != http.StatusOK { 172 | return nil, fmt.Errorf("failure to verify calls") 173 | } 174 | defer resp.Body.Close() 175 | 176 | var calls []Call 177 | if err = json.NewDecoder(resp.Body).Decode(&calls); err != nil { 178 | return nil, err 179 | } 180 | return calls, nil 181 | } 182 | 183 | // Clear assured calls for a Method and Path 184 | func (c *Client) Clear(method, path string) error { 185 | req, err := http.NewRequest(method, fmt.Sprintf("%s/clear/%s", c.url(), path), nil) 186 | if err != nil { 187 | return err 188 | } 189 | _, err = c.httpClient.Do(req) 190 | return err 191 | } 192 | 193 | // ClearAll clears all assured calls 194 | func (c *Client) ClearAll() error { 195 | req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/clear", c.url()), nil) 196 | if err != nil { 197 | return err 198 | } 199 | _, err = c.httpClient.Do(req) 200 | return err 201 | } 202 | -------------------------------------------------------------------------------- /pkg/assured/client_test.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "io" 8 | "net/http" 9 | "net/http/httptest" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestClient(t *testing.T) { 19 | client := NewClient(WithPort(9091)) 20 | go func() { _ = client.Serve() }() 21 | defer client.Close() 22 | time.Sleep(time.Second) 23 | 24 | url := client.URL() 25 | require.Equal(t, "http://localhost:9091/when", url) 26 | require.NoError(t, client.Given(*testCall1())) 27 | require.NoError(t, client.Given(*testCall2())) 28 | require.NoError(t, client.Given(*testCall3())) 29 | 30 | req, err := http.NewRequest(http.MethodGet, url+"/test/assured", bytes.NewReader([]byte(`{"calling":"you"}`))) 31 | require.NoError(t, err) 32 | 33 | resp, err := http.DefaultClient.Do(req) 34 | require.NoError(t, err) 35 | require.Equal(t, http.StatusOK, resp.StatusCode) 36 | body, err := io.ReadAll(resp.Body) 37 | require.NoError(t, err) 38 | require.Equal(t, []byte(`{"assured": true}`), body) 39 | 40 | req, err = http.NewRequest(http.MethodGet, url+"/test/assured", bytes.NewReader([]byte(`{"calling":"again"}`))) 41 | require.NoError(t, err) 42 | 43 | resp, err = http.DefaultClient.Do(req) 44 | require.NoError(t, err) 45 | require.Equal(t, http.StatusConflict, resp.StatusCode) 46 | body, err = io.ReadAll(resp.Body) 47 | require.NoError(t, err) 48 | require.Equal(t, []byte("error"), body) 49 | 50 | req, err = http.NewRequest(http.MethodPost, url+"/teapot/assured", bytes.NewReader([]byte(`{"calling":"here"}`))) 51 | require.NoError(t, err) 52 | 53 | resp, err = http.DefaultClient.Do(req) 54 | require.NoError(t, err) 55 | require.Equal(t, http.StatusTeapot, resp.StatusCode) 56 | body, err = io.ReadAll(resp.Body) 57 | require.NoError(t, err) 58 | require.Equal(t, []byte{}, body) 59 | 60 | calls, err := client.Verify("GET", "test/assured") 61 | require.NoError(t, err) 62 | require.Equal(t, []Call{ 63 | { 64 | Method: "GET", 65 | Path: "test/assured", 66 | StatusCode: 200, 67 | Response: []byte(`{"calling":"you"}`), 68 | Headers: map[string]string{"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}}, 69 | { 70 | Method: "GET", 71 | Path: "test/assured", 72 | StatusCode: 200, 73 | Response: []byte(`{"calling":"again"}`), 74 | Headers: map[string]string{"Content-Length": "19", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}}}, calls) 75 | 76 | calls, err = client.Verify("POST", "teapot/assured") 77 | require.NoError(t, err) 78 | require.Equal(t, []Call{ 79 | { 80 | Method: "POST", 81 | Path: "teapot/assured", 82 | StatusCode: 200, 83 | Response: []byte(`{"calling":"here"}`), 84 | Headers: map[string]string{"Content-Length": "18", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}}}, calls) 85 | 86 | err = client.Clear("GET", "test/assured") 87 | require.NoError(t, err) 88 | 89 | calls, err = client.Verify("GET", "test/assured") 90 | require.NoError(t, err) 91 | require.Nil(t, calls) 92 | 93 | calls, err = client.Verify("POST", "teapot/assured") 94 | require.NoError(t, err) 95 | require.Equal(t, []Call{ 96 | { 97 | Method: "POST", 98 | Path: "teapot/assured", 99 | StatusCode: 200, 100 | Response: []byte(`{"calling":"here"}`), 101 | Headers: map[string]string{"Content-Length": "18", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}}}, calls) 102 | 103 | err = client.ClearAll() 104 | require.NoError(t, err) 105 | 106 | calls, err = client.Verify("GET", "test/assured") 107 | require.NoError(t, err) 108 | require.Nil(t, calls) 109 | 110 | calls, err = client.Verify("POST", "teapot/assured") 111 | require.NoError(t, err) 112 | require.Nil(t, calls) 113 | } 114 | 115 | func TestClientTLS(t *testing.T) { 116 | insecureClient := http.Client{Transport: &http.Transport{ 117 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 118 | }} 119 | client := NewClient(WithTLS("testdata/localhost.pem", "testdata/localhost-key.pem"), WithPort(9092), 120 | WithHTTPClient(insecureClient)) 121 | go func() { _ = client.Serve() }() 122 | defer client.Close() 123 | time.Sleep(1 * time.Second) 124 | 125 | url := client.URL() 126 | require.Equal(t, "https://localhost:9092/when", url) 127 | require.NoError(t, client.Given(*testCall1())) 128 | 129 | req, err := http.NewRequest(http.MethodGet, url+"/test/assured", bytes.NewReader([]byte(`{"calling":"you"}`))) 130 | require.NoError(t, err) 131 | 132 | resp, err := insecureClient.Do(req) 133 | require.NoError(t, err) 134 | require.Equal(t, http.StatusOK, resp.StatusCode) 135 | body, err := io.ReadAll(resp.Body) 136 | require.NoError(t, err) 137 | require.Equal(t, []byte(`{"assured": true}`), body) 138 | 139 | calls, err := client.Verify("GET", "test/assured") 140 | require.NoError(t, err) 141 | require.Equal(t, []Call{ 142 | { 143 | Method: "GET", 144 | Path: "test/assured", 145 | StatusCode: 200, 146 | Response: []byte(`{"calling":"you"}`), 147 | Headers: map[string]string{"Content-Length": "17", "User-Agent": "Go-http-client/1.1", "Accept-Encoding": "gzip"}, 148 | }, 149 | }, calls) 150 | } 151 | 152 | func TestClientInvalidPort(t *testing.T) { 153 | client := NewClient(WithPort(-1)) 154 | 155 | require.Error(t, client.Serve()) 156 | } 157 | 158 | func TestClientCallbacks(t *testing.T) { 159 | httpClient := http.Client{} 160 | called := false 161 | delayCalled := false 162 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 163 | body, err := io.ReadAll(r.Body) 164 | require.NoError(t, err) 165 | require.Equal(t, []byte(`{"done":"here"}`), body) 166 | require.NotEmpty(t, r.Header.Get("x-info")) 167 | called = true 168 | })) 169 | delayTestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 170 | body, err := io.ReadAll(r.Body) 171 | require.NoError(t, err) 172 | require.Equal(t, []byte(`{"wait":"there's more"}`), body) 173 | delayCalled = true 174 | })) 175 | client := NewClient() 176 | go func() { _ = client.Serve() }() 177 | defer client.Close() 178 | time.Sleep(time.Second) 179 | 180 | require.NoError(t, client.Given(Call{ 181 | Path: "test/assured", 182 | Method: "POST", 183 | Delay: 2, 184 | Callbacks: []Callback{ 185 | { 186 | Method: "POST", 187 | Target: testServer.URL, 188 | Response: []byte(`{"done":"here"}`), 189 | Headers: map[string]string{"x-info": "important"}, 190 | }, 191 | { 192 | Method: "POST", 193 | Target: delayTestServer.URL, 194 | Delay: 4, 195 | Response: []byte(`{"wait":"there's more"}`), 196 | }, 197 | }, 198 | })) 199 | 200 | req, err := http.NewRequest(http.MethodPost, client.URL()+"/test/assured", bytes.NewReader([]byte(`{"calling":"here"}`))) 201 | require.NoError(t, err) 202 | 203 | start := time.Now() 204 | _, err = httpClient.Do(req) 205 | require.NoError(t, err) 206 | 207 | require.True(t, time.Since(start) >= 2*time.Second, "response should be delayed 2 seconds") 208 | // allow go routine to finish 209 | time.Sleep(1 * time.Second) 210 | require.True(t, called, "callback was not hit") 211 | require.False(t, delayCalled, "delayed callback should not be hit yet") 212 | time.Sleep(2 * time.Second) 213 | require.True(t, delayCalled, "delayed callback was not hit") 214 | } 215 | 216 | func TestClientClose(t *testing.T) { 217 | client := NewClient() 218 | go func() { _ = client.Serve() }() 219 | client2 := NewClient() 220 | go func() { _ = client2.Serve() }() 221 | time.Sleep(time.Second) 222 | 223 | require.NotEqual(t, client.URL(), client2.URL()) 224 | 225 | require.NoError(t, client.Given(*testCall1())) 226 | require.NoError(t, client2.Given(*testCall1())) 227 | 228 | client.Close() 229 | time.Sleep(time.Second) 230 | err := client.Given(*testCall1()) 231 | 232 | require.Error(t, err) 233 | require.Contains(t, err.Error(), `connection refused`) 234 | 235 | client2.Close() 236 | time.Sleep(time.Second) 237 | err = client2.Given(*testCall1()) 238 | 239 | require.Error(t, err) 240 | require.Contains(t, err.Error(), `connection refused`) 241 | } 242 | 243 | func TestClientGivenNoMethod(t *testing.T) { 244 | client := NewClientServe() 245 | defer client.Close() 246 | time.Sleep(time.Second) 247 | 248 | err := client.Given(Call{Path: "NoMethodMan"}) 249 | require.NoError(t, err) 250 | 251 | req, err := http.NewRequest(http.MethodGet, client.URL()+"/NoMethodMan", nil) 252 | require.NoError(t, err) 253 | 254 | resp, err := http.DefaultClient.Do(req) 255 | require.NoError(t, err) 256 | require.Equal(t, http.StatusOK, resp.StatusCode) 257 | } 258 | 259 | func TestClientGivenCallbackMissingTarget(t *testing.T) { 260 | call := Call{ 261 | Method: "POST", 262 | Callbacks: []Callback{ 263 | {Method: "POST"}, 264 | }, 265 | } 266 | client := NewClient() 267 | go func() { _ = client.Serve() }() 268 | defer client.Close() 269 | 270 | err := client.Given(call) 271 | 272 | require.Error(t, err) 273 | require.Equal(t, "cannot stub callback without target", err.Error()) 274 | } 275 | 276 | func TestClientGivenCallbackBadMethod(t *testing.T) { 277 | call := Call{ 278 | Method: "POST", 279 | Callbacks: []Callback{ 280 | {Method: "\"", Target: "http://localhost/"}, 281 | }, 282 | } 283 | client := NewClient() 284 | go func() { _ = client.Serve() }() 285 | defer client.Close() 286 | 287 | err := client.Given(call) 288 | 289 | require.Error(t, err) 290 | require.Equal(t, "net/http: invalid method \"\\\"\"", err.Error()) 291 | } 292 | 293 | func TestClientBadRequestFailure(t *testing.T) { 294 | client := NewClient() 295 | go func() { _ = client.Serve() }() 296 | defer client.Close() 297 | 298 | err := client.Given(Call{Method: "\"", Path: "goat/path"}) 299 | 300 | require.Error(t, err) 301 | require.Equal(t, `net/http: invalid method "\""`, err.Error()) 302 | 303 | err = client.Given(Call{Method: "\"", Path: "goat/path", Response: []byte("goats among men")}) 304 | 305 | require.Error(t, err) 306 | require.Equal(t, `net/http: invalid method "\""`, err.Error()) 307 | 308 | calls, err := client.Verify("\"", "goat/path") 309 | 310 | require.Error(t, err) 311 | require.Equal(t, `net/http: invalid method "\""`, err.Error()) 312 | require.Nil(t, calls) 313 | 314 | err = client.Clear("\"", "goat/path") 315 | 316 | require.Error(t, err) 317 | require.Equal(t, `net/http: invalid method "\""`, err.Error()) 318 | 319 | client.Port = -1 320 | err = client.ClearAll() 321 | 322 | require.Error(t, err) 323 | require.Equal(t, `parse "http://localhost:-1/clear": invalid port ":-1" after host`, err.Error()) 324 | } 325 | 326 | func TestClientVerifyHttpClientFailure(t *testing.T) { 327 | client := NewClient() 328 | go func() { _ = client.Serve() }() 329 | client.Close() 330 | 331 | calls, err := client.Verify("GONE", "not/started") 332 | 333 | require.Error(t, err) 334 | require.Contains(t, err.Error(), `connection refused`) 335 | require.Nil(t, calls) 336 | } 337 | 338 | func TestClientVerifyResponseFailure(t *testing.T) { 339 | client := NewClient() 340 | go func() { _ = client.Serve() }() 341 | defer client.Close() 342 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 343 | w.WriteHeader(http.StatusNotFound) 344 | })) 345 | defer testServer.Close() 346 | index := strings.LastIndex(testServer.URL, ":") 347 | port, err := strconv.ParseInt(testServer.URL[index+1:], 10, 64) 348 | require.NoError(t, err) 349 | client.Port = int(port) 350 | 351 | calls, err := client.Verify("GONE", "not/started") 352 | 353 | require.Error(t, err) 354 | require.Equal(t, `failure to verify calls`, err.Error()) 355 | require.Nil(t, calls) 356 | } 357 | 358 | func TestClientVerifyBodyFailure(t *testing.T) { 359 | client := NewClient() 360 | go func() { _ = client.Serve() }() 361 | defer client.Close() 362 | testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 363 | w.WriteHeader(http.StatusOK) 364 | _ = json.NewEncoder(w).Encode("ydob+dab") 365 | })) 366 | defer testServer.Close() 367 | index := strings.LastIndex(testServer.URL, ":") 368 | port, err := strconv.ParseInt(testServer.URL[index+1:], 10, 64) 369 | require.NoError(t, err) 370 | client.Port = int(port) 371 | 372 | calls, err := client.Verify("BODY", "bad+body") 373 | 374 | require.Error(t, err) 375 | require.Equal(t, `json: cannot unmarshal string into Go value of type []assured.Call`, err.Error()) 376 | require.Nil(t, calls) 377 | } 378 | 379 | func TestClientPathSanitization(t *testing.T) { 380 | client := NewClient() 381 | go func() { _ = client.Serve() }() 382 | defer client.Close() 383 | time.Sleep(time.Second) 384 | 385 | require.NoError(t, client.Given(Call{Method: "GET", Path: "///yoyo/path///", StatusCode: http.StatusAccepted})) 386 | 387 | req, err := http.NewRequest(http.MethodGet, client.URL()+"/yoyo/path", nil) 388 | require.NoError(t, err) 389 | 390 | resp, err := http.DefaultClient.Do(req) 391 | require.NoError(t, err) 392 | require.Equal(t, http.StatusAccepted, resp.StatusCode) 393 | } 394 | -------------------------------------------------------------------------------- /pkg/assured/handlers.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log/slog" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | type APIError struct { 13 | Error error `json:"error"` 14 | } 15 | 16 | func handleHealth(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(http.StatusOK) 18 | } 19 | 20 | // handleGiven is used to stub out a call for a given path 21 | func handleGiven(logger *slog.Logger, assuredCalls *CallStore) http.HandlerFunc { 22 | return func(w http.ResponseWriter, r *http.Request) { 23 | call := decodeAssuredCall(r) 24 | 25 | assuredCalls.Add(&call) 26 | logger.With("path", call.ID()).Info("assured call set") 27 | 28 | _ = encode[Call](w, http.StatusOK, call) 29 | } 30 | } 31 | 32 | // handleGivenCallback is used to stub out callbacks for a callback key 33 | func handleGivenCallback(logger *slog.Logger, callbackCalls *CallStore) http.HandlerFunc { 34 | return func(w http.ResponseWriter, r *http.Request) { 35 | call, err := decodeAssuredCallback(r) 36 | if err != nil { 37 | _ = encode[APIError](w, http.StatusBadRequest, APIError{err}) 38 | } 39 | 40 | callbackCalls.AddAt(call.Headers[AssuredCallbackKey], &call) 41 | logger.With("key", call.Headers[AssuredCallbackKey], "target", call.Headers[AssuredCallbackTarget]).Info("assured callback set") 42 | 43 | _ = encode[Call](w, http.StatusOK, call) 44 | } 45 | } 46 | 47 | // handleWhen is used to respond to a given assured call 48 | func handleWhen(logger *slog.Logger, httpClient *http.Client, assuredCalls, madeCalls, callbackCalls *CallStore, trackMadeCalls bool) http.HandlerFunc { 49 | return func(w http.ResponseWriter, r *http.Request) { 50 | call := decodeAssuredCall(r) 51 | calls := assuredCalls.Get(call.ID()) 52 | if len(calls) == 0 { 53 | logger.With("path", call.ID()).Info("assured call not found") 54 | _ = encode[APIError](w, http.StatusNotFound, APIError{errors.New("No assured calls")}) 55 | return 56 | } 57 | 58 | if trackMadeCalls { 59 | madeCalls.Add(&call) 60 | } 61 | assured := calls[0] 62 | assuredCalls.Rotate(assured) 63 | 64 | // Trigger callbacks, if applicable 65 | for _, callback := range callbackCalls.Get(assured.Headers[AssuredCallbackKey]) { 66 | go sendCallback(logger, httpClient, callback.Headers[AssuredCallbackTarget], callback) 67 | } 68 | 69 | // Delay response 70 | if delay, err := strconv.ParseInt(assured.Headers[AssuredDelay], 10, 64); err == nil { 71 | time.Sleep(time.Duration(delay) * time.Second) 72 | } 73 | 74 | logger.With("path", call.ID()).Info("assured call responded") 75 | _ = encodeAssuredCall(w, assured) 76 | } 77 | } 78 | 79 | // handleVerify returns all matching assured calls, used to verify a particular call 80 | func handleVerify(logger *slog.Logger, madeCalls *CallStore, trackMadeCalls bool) http.HandlerFunc { 81 | return func(w http.ResponseWriter, r *http.Request) { 82 | call := decodeAssuredCall(r) 83 | 84 | if !trackMadeCalls { 85 | _ = encode[APIError](w, http.StatusNotFound, APIError{errors.New("Tracking made calls is disabled")}) 86 | return 87 | } 88 | 89 | calls := madeCalls.Get(call.ID()) 90 | _ = encodeAssuredCall(w, calls) 91 | } 92 | } 93 | 94 | // handleClear is used to clear a specific assured call 95 | func handleClear(logger *slog.Logger, assuredCalls, madeCalls, callbackCalls *CallStore, trackMadeCalls bool) http.HandlerFunc { 96 | return func(w http.ResponseWriter, r *http.Request) { 97 | call := decodeAssuredCall(r) 98 | 99 | assuredCalls.Clear(call.ID()) 100 | madeCalls.Clear(call.ID()) 101 | logger.With("path", call.ID()).Info("cleared calls for path") 102 | if call.Headers[AssuredCallbackKey] != "" { 103 | callbackCalls.Clear(call.Headers[AssuredCallbackKey]) 104 | logger.With("key", call.Headers[AssuredCallbackKey]).Info("cleared calls for key") 105 | } 106 | } 107 | } 108 | 109 | // handleClearAll is used to clear all assured calls 110 | func handleClearAll(logger *slog.Logger, assuredCalls, madeCalls, callbackCalls *CallStore, trackMadeCalls bool) http.HandlerFunc { 111 | return func(w http.ResponseWriter, r *http.Request) { 112 | assuredCalls.ClearAll() 113 | madeCalls.ClearAll() 114 | callbackCalls.ClearAll() 115 | logger.Info("cleared all calls") 116 | } 117 | } 118 | 119 | // sendCallback sends a given callback to its target 120 | func sendCallback(logger *slog.Logger, httpClient *http.Client, target string, call *Call) { 121 | var delay int64 122 | if delayOverride, err := strconv.ParseInt(call.Headers[AssuredCallbackDelay], 10, 64); err == nil { 123 | delay = delayOverride 124 | } 125 | req, err := http.NewRequest(call.Method, target, bytes.NewBuffer(call.Response)) 126 | if err != nil { 127 | logger.With("target", target, "error", err).Info("failed to build callback request") 128 | return 129 | } 130 | for key, value := range call.Headers { 131 | req.Header.Set(key, value) 132 | } 133 | // Delay callback, if applicable 134 | time.Sleep(time.Duration(delay) * time.Second) 135 | resp, err := httpClient.Do(req) 136 | if err != nil { 137 | logger.With("target", target, "error", err).Info("failed to reach callback target") 138 | return 139 | } 140 | logger.With("target", target, "status_code", resp.StatusCode).Info("sent callback to target") 141 | } 142 | -------------------------------------------------------------------------------- /pkg/assured/options.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | ) 7 | 8 | var DefaultOptions = Options{ 9 | httpClient: http.DefaultClient, 10 | host: "localhost", 11 | trackMadeCalls: true, 12 | logger: slog.Default(), 13 | } 14 | 15 | // Option is a function on that configures rest assured settings 16 | type Option func(*Options) 17 | 18 | // Options can be used to configure the rest assured client. 19 | type Options struct { 20 | // httpClient used to interact with the rest assured server 21 | httpClient *http.Client 22 | 23 | // set the hostname to use in the client. Defaults to localhost. 24 | host string 25 | 26 | // port for the rest assured server to listen on. Defaults to any available port. 27 | Port int 28 | 29 | // tlsCertFile is the location of the tls cert for serving https. 30 | tlsCertFile string 31 | 32 | // tlsKeyFile is the location of the tls key for serving https. 33 | tlsKeyFile string 34 | 35 | // trackMadeCalls toggles storing the requests made against the rest assured server. Defaults to true. 36 | trackMadeCalls bool 37 | 38 | // logger to use for logging. Defaults the default logger. 39 | logger *slog.Logger 40 | } 41 | 42 | // WithHTTPClient sets the http client option. 43 | func WithHTTPClient(c http.Client) Option { 44 | return func(o *Options) { 45 | o.httpClient = &c 46 | } 47 | } 48 | 49 | // WithHost sets the host option. 50 | func WithHost(h string) Option { 51 | return func(o *Options) { 52 | if h != "" { 53 | o.host = h 54 | } 55 | } 56 | } 57 | 58 | // WithPort sets the port option. 59 | func WithPort(p int) Option { 60 | return func(o *Options) { 61 | if p != 0 { 62 | o.Port = p 63 | } 64 | } 65 | } 66 | 67 | // WithTLS sets the tls options. 68 | func WithTLS(cert, key string) Option { 69 | return func(o *Options) { 70 | o.tlsCertFile = cert 71 | o.tlsKeyFile = key 72 | } 73 | } 74 | 75 | // WithCallTracking sets the trackMadeCalls option. 76 | func WithCallTracking(t bool) Option { 77 | return func(o *Options) { 78 | o.trackMadeCalls = t 79 | } 80 | } 81 | 82 | // WithCallTracking sets the trackMadeCalls option. 83 | func WithLogger(l *slog.Logger) Option { 84 | return func(o *Options) { 85 | if l != nil { 86 | o.logger = l 87 | } 88 | } 89 | } 90 | 91 | func (o *Options) applyOptions(opts ...Option) { 92 | for _, opt := range opts { 93 | opt(o) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/assured/options_test.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | "os" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func Test_applyOptions(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | option Option 15 | want Options 16 | }{ 17 | { 18 | name: "with http client", 19 | option: WithHTTPClient(*http.DefaultClient), 20 | want: Options{ 21 | httpClient: http.DefaultClient, 22 | }, 23 | }, 24 | { 25 | name: "with host", 26 | option: WithHost("rest-assured"), 27 | want: Options{ 28 | host: "rest-assured", 29 | }, 30 | }, 31 | { 32 | name: "with port", 33 | option: WithPort(8889), 34 | want: Options{ 35 | Port: 8889, 36 | }, 37 | }, 38 | { 39 | name: "with track", 40 | option: WithCallTracking(true), 41 | want: Options{ 42 | trackMadeCalls: true, 43 | }, 44 | }, 45 | { 46 | name: "with logger", 47 | option: WithLogger(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))), 48 | want: Options{ 49 | logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})), 50 | }, 51 | }, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | o := Options{} 56 | o.applyOptions(tt.option) 57 | if !reflect.DeepEqual(o, tt.want) { 58 | t.Errorf("applyOptions() = %v, want %v", o, tt.want) 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/assured/routes.go: -------------------------------------------------------------------------------- 1 | package assured 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "net/http" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | func routes( 14 | logger *slog.Logger, 15 | assuredCalls *CallStore, 16 | madeCalls *CallStore, 17 | callbackCalls *CallStore, 18 | httpClient *http.Client, 19 | trackMadeCalls bool, 20 | ) *http.ServeMux { 21 | mux := http.NewServeMux() 22 | 23 | mux.HandleFunc("/health", handleHealth) 24 | mux.HandleFunc("/given/{path...}", handleGiven(logger, assuredCalls)) 25 | mux.HandleFunc("/callback", handleGivenCallback(logger, callbackCalls)) 26 | mux.HandleFunc("/when/{path...}", handleWhen(logger, httpClient, assuredCalls, madeCalls, callbackCalls, trackMadeCalls)) 27 | mux.HandleFunc("/verify/{path...}", handleVerify(logger, madeCalls, trackMadeCalls)) 28 | mux.HandleFunc("/clear/{path...}", handleClear(logger, assuredCalls, madeCalls, callbackCalls, trackMadeCalls)) 29 | mux.HandleFunc("/clear", handleClearAll(logger, assuredCalls, madeCalls, callbackCalls, trackMadeCalls)) 30 | 31 | return mux 32 | } 33 | 34 | // func decode[T any](r *http.Request) (T, error) { 35 | // var v T 36 | // if err := json.NewDecoder(r.Body).Decode(&v); err != nil { 37 | // return v, fmt.Errorf("decode json: %w", err) 38 | // } 39 | // return v, nil 40 | // } 41 | 42 | func encode[T any](w http.ResponseWriter, status int, v T) error { 43 | w.Header().Set("Content-Type", "application/json") 44 | w.WriteHeader(status) 45 | if err := json.NewEncoder(w).Encode(v); err != nil { 46 | return fmt.Errorf("encode json: %w", err) 47 | } 48 | return nil 49 | } 50 | 51 | // decodeAssuredCall converts an http request into an assured Call object 52 | func decodeAssuredCall(req *http.Request) Call { 53 | method := req.Method 54 | if m := req.Header.Get(AssuredMethod); m != "" { 55 | method = m 56 | } 57 | 58 | call := Call{ 59 | Path: req.PathValue("path"), 60 | Method: method, 61 | StatusCode: http.StatusOK, 62 | } 63 | 64 | // Set status code override 65 | if statusCode, err := strconv.ParseInt(req.Header.Get(AssuredStatus), 10, 64); err == nil { 66 | call.StatusCode = int(statusCode) 67 | } 68 | 69 | // Set headers 70 | headers := map[string]string{} 71 | for key, value := range req.Header { 72 | headers[key] = value[0] 73 | } 74 | call.Headers = headers 75 | 76 | // Set query 77 | query := map[string]string{} 78 | for key, value := range req.URL.Query() { 79 | query[key] = value[0] 80 | } 81 | call.Query = query 82 | 83 | // Set response body 84 | if req.Body != nil { 85 | defer req.Body.Close() 86 | if bytes, err := io.ReadAll(req.Body); err == nil { 87 | call.Response = bytes 88 | } 89 | } 90 | 91 | return call 92 | } 93 | 94 | // decodeAssuredCallback converts an http request into an assured Callback object 95 | func decodeAssuredCallback(req *http.Request) (Call, error) { 96 | call := Call{ 97 | Method: req.Method, 98 | StatusCode: http.StatusCreated, 99 | } 100 | 101 | // Require headers 102 | if len(req.Header[AssuredCallbackKey]) == 0 { 103 | return call, fmt.Errorf("'%s' header required for callback", AssuredCallbackKey) 104 | } 105 | if len(req.Header[AssuredCallbackTarget]) == 0 { 106 | return call, fmt.Errorf("'%s' header required for callback", AssuredCallbackTarget) 107 | } 108 | 109 | // Set headers 110 | headers := map[string]string{} 111 | for key, value := range req.Header { 112 | headers[key] = value[0] 113 | } 114 | call.Headers = headers 115 | 116 | // Set response body 117 | if req.Body != nil { 118 | defer req.Body.Close() 119 | if bytes, err := io.ReadAll(req.Body); err == nil { 120 | call.Response = bytes 121 | } 122 | } 123 | 124 | return call, nil 125 | } 126 | 127 | // encodeAssuredCall writes the assured Call to the http response as it is intended to be stubbed 128 | func encodeAssuredCall(w http.ResponseWriter, i interface{}) error { 129 | switch resp := i.(type) { 130 | case *Call: 131 | for key, value := range resp.Headers { 132 | if !strings.HasPrefix(key, "Assured-") { 133 | w.Header().Set(key, value) 134 | } 135 | } 136 | w.WriteHeader(resp.StatusCode) 137 | _, _ = w.Write([]byte(resp.String())) 138 | case []*Call: 139 | w.Header().Set("Content-Type", "application/json") 140 | return json.NewEncoder(w).Encode(resp) 141 | } 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /pkg/assured/testdata/assured.json: -------------------------------------------------------------------------------- 1 | {"assured": true} -------------------------------------------------------------------------------- /pkg/assured/testdata/callbacks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "test/assured", 4 | "method": "GET", 5 | "status_code": 201, 6 | "delay": 0, 7 | "response": "testdata/assured.json", 8 | "headers": { 9 | "Content-Length": "17", 10 | "User-Agent": "Go-http-client/1.1", 11 | "Accept-Encoding": "gzip" 12 | } 13 | }, 14 | { 15 | "path": "test/assured", 16 | "method": "GET", 17 | "status_code": 200, 18 | "delay": 0, 19 | "response": "testdata/image.jpg", 20 | "headers": { 21 | "Content-Length": "56000", 22 | "Content-Type": "image/jpeg", 23 | "User-Agent": "Go-http-client/1.1" 24 | } 25 | }, 26 | { 27 | "path": "teapot/assured", 28 | "method": "POST", 29 | "status_code": 418, 30 | "delay": 2, 31 | "headers": { 32 | "Content-Length": "0", 33 | "User-Agent": "Go-http-client/1.1", 34 | "Accept-Encoding": "gzip" 35 | }, 36 | "callbacks": [ 37 | { 38 | "target": "http://localhost:9000/when/capture", 39 | "method": "PUT", 40 | "delay": 5, 41 | "response": "Therefore do not worry about tomorrow, for tomorrow will worry about itself. Each day has enough trouble of its own." 42 | } 43 | ] 44 | } 45 | ] 46 | -------------------------------------------------------------------------------- /pkg/assured/testdata/calls.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "test/assured", 4 | "method": "GET", 5 | "status_code": 200, 6 | "delay": 0, 7 | "response": "eyJhc3N1cmVkIjogdHJ1ZX0=", 8 | "headers": { 9 | "Content-Length": "17", 10 | "User-Agent": "Go-http-client/1.1", 11 | "Accept-Encoding": "gzip" 12 | }, 13 | "query": { 14 | "assured": "max" 15 | } 16 | }, 17 | { 18 | "path": "test/assured", 19 | "method": "GET", 20 | "status_code": 409, 21 | "delay": 0, 22 | "response": "ZXJyb3I=", 23 | "headers": { 24 | "Content-Length": "5", 25 | "User-Agent": "Go-http-client/1.1", 26 | "Accept-Encoding": "gzip" 27 | } 28 | }, 29 | { 30 | "path": "teapot/assured", 31 | "method": "POST", 32 | "status_code": 418, 33 | "delay": 0, 34 | "headers": { 35 | "Content-Length": "0", 36 | "User-Agent": "Go-http-client/1.1", 37 | "Accept-Encoding": "gzip" 38 | } 39 | } 40 | ] 41 | -------------------------------------------------------------------------------- /pkg/assured/testdata/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jesse0Michael/go-rest-assured/9d913e2243912800c26b44065a1ae1aee47152dc/pkg/assured/testdata/image.jpg -------------------------------------------------------------------------------- /pkg/assured/testdata/localhost-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDSzqdadKCIpZXM 3 | ttze7h9mjFXGp3LIrH0TOj0Ljd/ZYYC1zMYa3+S/K+A1C88h+CP/RyCxHWd8cSdr 4 | mufcWf6CxYxlQZ1+C2OCMa0ppyqeqA8lUmARnKtpWbZKtKVrNWdKchBtrq1JLz5H 5 | vl6x6aVXzirTP1qjklge1n06dmrm1hq5bz59qChliVH/edlVJHhqSHxePCWcF9Ju 6 | 5LNwfr2tQ6tKywYPQhN1cPWF3XM0eeox8zJwVZVESYex/2ATZNEx21in7/xGu9Fn 7 | bb3HydmP1hWHUAjFd8n8CfuHQEmQI6mwIA1NiSdL2xm6fHaX14z4SZUULO3RUQBq 8 | ano6A8DzAgMBAAECggEARSaY6yk6x9hZbU/G4DIIhuEd+g5Prtg2JyqBf9rIRUyg 9 | pgJIMeSj6rn/0CbRiXkWhdTVOoREwCN2JOwphrTWshsPVN/TQ9eEit60yOcXKfhD 10 | D6EX9m8II4TUMWlUofhgq6EDFpeoLEudCHfAJGhaXpU4oxczKlkFIwiFB7p0kser 11 | 92lWRrAN05obVMy6n9oI6iFK9IAav3wpHWSkdFbMuSo508RFXhWO7vgYRAQQe1QJ 12 | s7m/mWXPSlNgzRzKZceQft8ly1IaStUY6hN8RgkGFCWp5KXQNDT/RLG/8dqZMgCy 13 | t1h9o0kIFAD5mV5fscIfRx3fa/WeKwaWHo9fEuglYQKBgQDUqVCT6ggmuPbf6oxK 14 | AmT1AnpwscT2EGOj9/OALhnGFK3uOzPzfZKbIGOJpA+chthj6QaWyzv5twMdyuAZ 15 | ajlCpxMb/Hh8nY8y5IIuQlywjX3pYp7PKP+fLIXGcfHMElHgmvxSfpl+s2VgdMZv 16 | vwrvSZjnLapt3cQg9y+YlCXpKwKBgQD9xJtlMypla5oy1sp2MLT6OO6jE1a7tXFY 17 | n+KUNArJWG/JnLMjZyODnWTJybK62CkS7BJRKHogWWU+APezCppARukMON1XWk1q 18 | xMp5znqdbrYIDuFS1RDuLaZN87gmaZO2RGySWOtIswayjVObDaZIjHPexfDQldMd 19 | 7hqLHtaTWQKBgB6VTvpGsO9osFBZed125K/7sEIevrWoe7vr/7/SYXCELtNQ/lKp 20 | H3lhug7kYj5fgiU0MPdkuPZbpH4XXA0ropp8Tpo6v9UkD6EBDdu7PXtCjfYPcclX 21 | GxP4IjD82ApCJDZJ87mlbUu0XwL1KEiPz/HDwtKUVatBNmCLS/QEckNLAoGAQLgF 22 | kZes9lSs7M5OsDnXzgD7C2gBNLofgCS2mmlzXspJKDfoj9zb99MKQ1RHWtOKFiNS 23 | wJP8IYfrBBcnFOhdUotDIDTYblC/cb1ijhIUv1Xe4ss4t/FS8gt+YF8J1fITE+b7 24 | cY9v4giWaq8UZXTjtZjsbCI7OlcbkoSJDHX+73ECgYBLW5rT/S5ljIMNaUAgzz7b 25 | RC2O+gq8oZ828ZjWHiYNnaxjP69z6+6WAC3uVSRKfBg+nV8sYIfX5uesp2sfHTD7 26 | IaYeNjYKKNn3tzWNKxfvR2I+mPo9JzKsDUB9Mlcpt/PlcQE4ZigcDVA9rF6Mp07L 27 | rRPSdlzC8q1LRNJwNEY16A== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /pkg/assured/testdata/localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEYDCCAsigAwIBAgIQZbcHJJ//g32ps6sKwtHVfzANBgkqhkiG9w0BAQsFADCB 3 | jTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTEwLwYDVQQLDChqZXNz 4 | ZW1pY2hhZWxAR1BTSS1NQUMtNTUgKEplc3NlIE1pY2hhZWwpMTgwNgYDVQQDDC9t 5 | a2NlcnQgamVzc2VtaWNoYWVsQEdQU0ktTUFDLTU1IChKZXNzZSBNaWNoYWVsKTAe 6 | Fw0xOTA2MDEwMDAwMDBaFw0zMDA5MDIxNjQ0NDRaMFwxJzAlBgNVBAoTHm1rY2Vy 7 | dCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTExMC8GA1UECwwoamVzc2VtaWNoYWVs 8 | QEdQU0ktTUFDLTU1IChKZXNzZSBNaWNoYWVsKTCCASIwDQYJKoZIhvcNAQEBBQAD 9 | ggEPADCCAQoCggEBANLOp1p0oIillcy23N7uH2aMVcancsisfRM6PQuN39lhgLXM 10 | xhrf5L8r4DULzyH4I/9HILEdZ3xxJ2ua59xZ/oLFjGVBnX4LY4IxrSmnKp6oDyVS 11 | YBGcq2lZtkq0pWs1Z0pyEG2urUkvPke+XrHppVfOKtM/WqOSWB7WfTp2aubWGrlv 12 | Pn2oKGWJUf952VUkeGpIfF48JZwX0m7ks3B+va1Dq0rLBg9CE3Vw9YXdczR56jHz 13 | MnBVlURJh7H/YBNk0THbWKfv/Ea70WdtvcfJ2Y/WFYdQCMV3yfwJ+4dASZAjqbAg 14 | DU2JJ0vbGbp8dpfXjPhJlRQs7dFRAGpqejoDwPMCAwEAAaNsMGowDgYDVR0PAQH/ 15 | BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwHwYDVR0j 16 | BBgwFoAUWCRvOnlHWtGELq+UFCuLC2Ukrd8wFAYDVR0RBA0wC4IJbG9jYWxob3N0 17 | MA0GCSqGSIb3DQEBCwUAA4IBgQB6eXLTq8ypIRvRvkrnbJ/KNGWYqZ2nLoZd4dc9 18 | jqj483EJQ6/3XGBpXpg+0yK1YejS44nIVmgz1iGG8OWwTiGiGmdDIJZfM6vIvtAS 19 | 2sZIc1J9ZnczE6bdWf1vcApATi9tSCAx15YHMUYB26E/7jucDVfT+LJlsxt72pTr 20 | PSxkXOqGGeeH/YTxOaBlrLuXaV1Qn1ZeUOhzLFZ/2V6QNoMmzSWEXmQ7zlMnDiVk 21 | S57uVQnI3Dw9dm89n1P67T78bFJ6aSPGZdBC0ZDnyj19DKen8HzT64sJ7FUZvWVR 22 | tX5OoxHEq8mY4m04FG7lzBj7JWkGaw2mA+VPcXrrz0EhSR7WjG4tJ+mCTTwEwcBd 23 | 08B/5DChavHplKCB+KOi42Q+kKTWgBZbBRe/2dFUp6iLGu1Pf89R00AI1EmmeCuF 24 | 7ax8wMS+W4zQ8Hq1kfi0zpxJJmAhYkCAB3poYz8XyH5Rh7o6yWVEbctX4XAQoxWy 25 | P0nrqS9HP0l0aDfjfnsc/53xcAM= 26 | -----END CERTIFICATE----- 27 | --------------------------------------------------------------------------------