| 1 | // Integration tests for every /api/v1/ endpoint exposed by gitly. |
| 2 | // |
| 3 | // The suite spawns its own gitly process on a non-default port using a |
| 4 | // dedicated sqlite database, so it can be executed independently of any |
| 5 | // long-running dev instance (run with `v test api/` or `v test .`). |
| 6 | // |
| 7 | // Endpoints covered: |
| 8 | // GET /api/v1/me |
| 9 | // GET /api/v1/users/:username |
| 10 | // GET /api/v1/users/:username/repos |
| 11 | // GET /api/v1/repos/:username/:repo_name |
| 12 | // GET /api/v1/repos/:username/:repo_name/issues |
| 13 | // POST /api/v1/repos/:username/:repo_name/issues |
| 14 | // GET /api/v1/repos/:username/:repo_name/issues/:id |
| 15 | // GET /api/v1/repos/:username/:repo_name/pulls |
| 16 | // GET /api/v1/repos/:username/:repo_name/pulls/:id |
| 17 | // GET /api/v1/repos/:username/:repo_name/pulls/:id/comments |
| 18 | // POST /api/v1/repos/:repo_id/star |
| 19 | // POST /api/v1/repos/:repo_id/watch |
| 20 | // GET /api/v1/repos/:repo_id_str/tree/files |
| 21 | // GET /api/v1/:user/:repo_name/branches/count |
| 22 | // GET /api/v1/:user/:repo_name/:branch_name/commits/count |
| 23 | // GET /api/v1/:username/:repo_name/issues/count |
| 24 | // POST /api/v1/users/avatar |
| 25 | // POST /api/v1/ci/status |
| 26 | module api |
| 27 | |
| 28 | import os |
| 29 | import log |
| 30 | import net.http |
| 31 | import time |
| 32 | import json |
| 33 | |
| 34 | const test_port = 8765 |
| 35 | const test_url = 'http://127.0.0.1:${test_port}' |
| 36 | const test_username = 'apitester' |
| 37 | const test_password = '1234zxcv' |
| 38 | const test_email = '[email protected]' |
| 39 | const test_repo = 'apitest' |
| 40 | const test_other_user = 'apitester2' |
| 41 | const test_other_password = '5678qwer' |
| 42 | const test_other_email = '[email protected]' |
| 43 | |
| 44 | const test_binary = 'gitly_apitest.exe' |
| 45 | const test_sqlite_path = 'gitly_apitest.sqlite' |
| 46 | |
| 47 | // Test-wide state is passed between testsuite_begin and individual tests via |
| 48 | // environment variables, since `v test` does not allow `__global` declarations |
| 49 | // in module test files. |
| 50 | const env_session = 'GITLY_APITEST_SESSION' |
| 51 | const env_other_session = 'GITLY_APITEST_OTHER_SESSION' |
| 52 | const env_bearer = 'GITLY_APITEST_BEARER' |
| 53 | const env_repo_id = 'GITLY_APITEST_REPO_ID' |
| 54 | |
| 55 | fn session_cookie() string { |
| 56 | return os.getenv(env_session) |
| 57 | } |
| 58 | |
| 59 | fn other_session_cookie() string { |
| 60 | return os.getenv(env_other_session) |
| 61 | } |
| 62 | |
| 63 | fn bearer_token() string { |
| 64 | return os.getenv(env_bearer) |
| 65 | } |
| 66 | |
| 67 | fn repo_id() int { |
| 68 | return os.getenv(env_repo_id).int() |
| 69 | } |
| 70 | |
| 71 | // -- testsuite plumbing ------------------------------------------------------- |
| 72 | |
| 73 | fn testsuite_begin() { |
| 74 | chdir_to_project_root() |
| 75 | kill_test_gitly() |
| 76 | cleanup_test_state() |
| 77 | ensure_gitly_binary() |
| 78 | spawn_test_gitly() |
| 79 | wait_for_test_gitly() |
| 80 | |
| 81 | session := register(test_username, test_password, test_email) or { |
| 82 | fail('register primary user: ${err}') |
| 83 | } |
| 84 | os.setenv(env_session, session, true) |
| 85 | |
| 86 | other := register(test_other_user, test_other_password, test_other_email) or { |
| 87 | fail('register secondary user: ${err}') |
| 88 | } |
| 89 | os.setenv(env_other_session, other, true) |
| 90 | |
| 91 | token := create_api_token(session, test_username) or { fail('create api token: ${err}') } |
| 92 | os.setenv(env_bearer, token, true) |
| 93 | |
| 94 | create_repo(session, test_repo) or { fail('create repo: ${err}') } |
| 95 | |
| 96 | rid := fetch_test_repo_id() or { fail('fetch repo id: ${err}') } |
| 97 | os.setenv(env_repo_id, rid.str(), true) |
| 98 | } |
| 99 | |
| 100 | fn testsuite_end() { |
| 101 | kill_test_gitly() |
| 102 | cleanup_test_state() |
| 103 | } |
| 104 | |
| 105 | @[noreturn] |
| 106 | fn fail(msg string) { |
| 107 | log.error('api endpoints_test: ${msg}') |
| 108 | kill_test_gitly() |
| 109 | cleanup_test_state() |
| 110 | exit(1) |
| 111 | } |
| 112 | |
| 113 | fn chdir_to_project_root() { |
| 114 | project_root := os.real_path(os.join_path(os.dir(@FILE), '..')) |
| 115 | os.chdir(project_root) or { fail('chdir to project root ${project_root}: ${err}') } |
| 116 | } |
| 117 | |
| 118 | fn cleanup_test_state() { |
| 119 | for ext in ['', '-shm', '-wal'] { |
| 120 | path := test_sqlite_path + ext |
| 121 | if os.exists(path) { |
| 122 | os.rm(path) or {} |
| 123 | } |
| 124 | } |
| 125 | for user in [test_username, test_other_user] { |
| 126 | repo_path := os.join_path('repos', user) |
| 127 | if os.exists(repo_path) { |
| 128 | os.rmdir_all(repo_path) or {} |
| 129 | } |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | fn ensure_gitly_binary() { |
| 134 | if os.exists(test_binary) { |
| 135 | return |
| 136 | } |
| 137 | log.info('building ${test_binary} ...') |
| 138 | res := os.execute('v -d sqlite -d use_libbacktrace -d use_openssl -o ${test_binary} .') |
| 139 | if res.exit_code != 0 { |
| 140 | fail('failed to build gitly: ${res.output}') |
| 141 | } |
| 142 | } |
| 143 | |
| 144 | fn spawn_test_gitly() { |
| 145 | os.setenv('GITLY_PORT', test_port.str(), true) |
| 146 | os.setenv('GITLY_SQLITE_PATH', test_sqlite_path, true) |
| 147 | spawn fn () { |
| 148 | os.execute('./${test_binary}') |
| 149 | }() |
| 150 | } |
| 151 | |
| 152 | fn wait_for_test_gitly() { |
| 153 | for i := 0; i < 100; i++ { |
| 154 | time.sleep(100 * time.millisecond) |
| 155 | http.get(test_url + '/') or { continue } |
| 156 | return |
| 157 | } |
| 158 | fail('gitly did not start listening on ${test_url}') |
| 159 | } |
| 160 | |
| 161 | fn kill_test_gitly() { |
| 162 | os.execute('pkill -9 ${test_binary}') |
| 163 | } |
| 164 | |
| 165 | // -- helpers ------------------------------------------------------------------ |
| 166 | |
| 167 | fn url(path string) string { |
| 168 | if path.starts_with('/') { |
| 169 | return '${test_url}${path}' |
| 170 | } |
| 171 | return '${test_url}/${path}' |
| 172 | } |
| 173 | |
| 174 | fn extract_token_cookie(h http.Header) string { |
| 175 | for v in h.values(.set_cookie) { |
| 176 | t := v.find_between('token=', ';') |
| 177 | if t != '' { |
| 178 | return t |
| 179 | } |
| 180 | } |
| 181 | return '' |
| 182 | } |
| 183 | |
| 184 | fn register(username string, password string, email string) !string { |
| 185 | body := 'username=${username}&password=${password}&email=${email}&no_redirect=1' |
| 186 | resp := http.post(url('/register'), body)! |
| 187 | if resp.status_code != 200 { |
| 188 | return error('register returned ${resp.status_code}: ${resp.body}') |
| 189 | } |
| 190 | tok := extract_token_cookie(resp.header) |
| 191 | if tok == '' { |
| 192 | return error('no session token cookie in register response') |
| 193 | } |
| 194 | return tok |
| 195 | } |
| 196 | |
| 197 | fn create_repo(token string, name string) ! { |
| 198 | resp := http.fetch( |
| 199 | method: .post |
| 200 | url: url('/new') |
| 201 | cookies: { |
| 202 | 'token': token |
| 203 | } |
| 204 | data: 'name=${name}&description=api+test&clone_url=&repo_visibility=public&no_redirect=1' |
| 205 | )! |
| 206 | if resp.status_code != 200 || resp.body != 'ok' { |
| 207 | return error('unexpected response ${resp.status_code}: ${resp.body}') |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | fn create_api_token(token string, username string) !string { |
| 212 | resp := http.fetch( |
| 213 | method: .post |
| 214 | url: url('/${username}/settings/api-tokens') |
| 215 | cookies: { |
| 216 | 'token': token |
| 217 | } |
| 218 | data: 'name=api-test' |
| 219 | allow_redirect: false |
| 220 | )! |
| 221 | if resp.status_code != 302 && resp.status_code != 303 { |
| 222 | return error('expected redirect, got ${resp.status_code}: ${resp.body}') |
| 223 | } |
| 224 | location := resp.header.get(.location) or { return error('no Location header') } |
| 225 | plain := location.all_after('new_token=') |
| 226 | if plain == '' || plain == location { |
| 227 | return error('could not parse new_token from ${location}') |
| 228 | } |
| 229 | return plain |
| 230 | } |
| 231 | |
| 232 | fn fetch_test_repo_id() !int { |
| 233 | resp := http.get(url('/api/v1/users/${test_username}/repos'))! |
| 234 | if resp.status_code != 200 { |
| 235 | return error('listing returned ${resp.status_code}') |
| 236 | } |
| 237 | repos := json.decode([]ApiRepoSummary, resp.body)! |
| 238 | for r in repos { |
| 239 | if r.name == test_repo { |
| 240 | return r.id |
| 241 | } |
| 242 | } |
| 243 | return error('repo not found in listing') |
| 244 | } |
| 245 | |
| 246 | struct ApiRepoSummary { |
| 247 | id int |
| 248 | name string |
| 249 | user_name string |
| 250 | } |
| 251 | |
| 252 | struct ApiUserSummary { |
| 253 | id int |
| 254 | username string |
| 255 | full_name string |
| 256 | avatar string |
| 257 | } |
| 258 | |
| 259 | struct ApiIssueSummary { |
| 260 | id int |
| 261 | number int |
| 262 | repo_id int |
| 263 | title string |
| 264 | body string |
| 265 | author string |
| 266 | status string |
| 267 | } |
| 268 | |
| 269 | struct ApiPullSummary { |
| 270 | id int |
| 271 | repo_id int |
| 272 | title string |
| 273 | description string |
| 274 | status string |
| 275 | } |
| 276 | |
| 277 | struct ApiCommentSummary { |
| 278 | id int |
| 279 | author string |
| 280 | text string |
| 281 | } |
| 282 | |
| 283 | struct ApiBoolResult { |
| 284 | success bool |
| 285 | result bool |
| 286 | } |
| 287 | |
| 288 | struct ApiFilesResult { |
| 289 | success bool |
| 290 | result []FileSummary |
| 291 | } |
| 292 | |
| 293 | struct FileSummary { |
| 294 | name string |
| 295 | last_msg string |
| 296 | last_hash string |
| 297 | last_time string |
| 298 | size string |
| 299 | } |
| 300 | |
| 301 | fn bearer_header() http.Header { |
| 302 | return http.new_header(key: .authorization, value: 'Bearer ${bearer_token()}') |
| 303 | } |
| 304 | |
| 305 | // -- tests -------------------------------------------------------------------- |
| 306 | |
| 307 | fn test_api_v1_me_requires_auth() { |
| 308 | resp := http.get(url('/api/v1/me')) or { panic(err) } |
| 309 | assert resp.status_code == 401 |
| 310 | } |
| 311 | |
| 312 | fn test_api_v1_me_with_bearer() { |
| 313 | resp := http.fetch( |
| 314 | method: .get |
| 315 | url: url('/api/v1/me') |
| 316 | header: bearer_header() |
| 317 | ) or { panic(err) } |
| 318 | assert resp.status_code == 200 |
| 319 | user := json.decode(ApiUserSummary, resp.body) or { panic(err) } |
| 320 | assert user.username == test_username |
| 321 | } |
| 322 | |
| 323 | fn test_api_v1_me_with_session_cookie() { |
| 324 | resp := http.fetch( |
| 325 | method: .get |
| 326 | url: url('/api/v1/me') |
| 327 | cookies: { |
| 328 | 'token': session_cookie() |
| 329 | } |
| 330 | ) or { panic(err) } |
| 331 | assert resp.status_code == 200 |
| 332 | user := json.decode(ApiUserSummary, resp.body) or { panic(err) } |
| 333 | assert user.username == test_username |
| 334 | } |
| 335 | |
| 336 | fn test_api_v1_user_lookup() { |
| 337 | resp := http.get(url('/api/v1/users/${test_username}')) or { panic(err) } |
| 338 | assert resp.status_code == 200 |
| 339 | user := json.decode(ApiUserSummary, resp.body) or { panic(err) } |
| 340 | assert user.username == test_username |
| 341 | |
| 342 | missing := http.get(url('/api/v1/users/ghost_user')) or { panic(err) } |
| 343 | assert missing.status_code == 404 |
| 344 | } |
| 345 | |
| 346 | fn test_api_v1_user_repos() { |
| 347 | resp := http.get(url('/api/v1/users/${test_username}/repos')) or { panic(err) } |
| 348 | assert resp.status_code == 200 |
| 349 | repos := json.decode([]ApiRepoSummary, resp.body) or { panic(err) } |
| 350 | assert repos.len >= 1 |
| 351 | mut found := false |
| 352 | for r in repos { |
| 353 | if r.name == test_repo { |
| 354 | found = true |
| 355 | break |
| 356 | } |
| 357 | } |
| 358 | assert found |
| 359 | } |
| 360 | |
| 361 | fn test_api_v1_repo_show() { |
| 362 | resp := http.get(url('/api/v1/repos/${test_username}/${test_repo}')) or { panic(err) } |
| 363 | assert resp.status_code == 200 |
| 364 | r := json.decode(ApiRepoSummary, resp.body) or { panic(err) } |
| 365 | assert r.name == test_repo |
| 366 | assert r.user_name == test_username |
| 367 | |
| 368 | missing := http.get(url('/api/v1/repos/${test_username}/nope')) or { panic(err) } |
| 369 | assert missing.status_code == 404 |
| 370 | } |
| 371 | |
| 372 | fn test_api_v1_repo_issues_list_empty() { |
| 373 | resp := http.get(url('/api/v1/repos/${test_username}/${test_repo}/issues')) or { panic(err) } |
| 374 | assert resp.status_code == 200 |
| 375 | issues := json.decode([]ApiIssueSummary, resp.body) or { panic(err) } |
| 376 | assert issues.len == 0 |
| 377 | } |
| 378 | |
| 379 | fn test_api_v1_create_issue_requires_auth() { |
| 380 | resp := http.post_form(url('/api/v1/repos/${test_username}/${test_repo}/issues'), { |
| 381 | 'title': 'should-fail' |
| 382 | 'body': 'no token' |
| 383 | }) or { panic(err) } |
| 384 | assert resp.status_code == 401 |
| 385 | } |
| 386 | |
| 387 | fn test_api_v1_create_issue_requires_title() { |
| 388 | resp := http.fetch( |
| 389 | method: .post |
| 390 | url: url('/api/v1/repos/${test_username}/${test_repo}/issues') |
| 391 | header: http.new_header_from_map({ |
| 392 | .authorization: 'Bearer ${bearer_token()}' |
| 393 | .content_type: 'application/x-www-form-urlencoded' |
| 394 | }) |
| 395 | data: 'body=missing-title' |
| 396 | ) or { panic(err) } |
| 397 | assert resp.status_code == 400 |
| 398 | } |
| 399 | |
| 400 | fn test_api_v1_create_issue_succeeds() { |
| 401 | resp := http.fetch( |
| 402 | method: .post |
| 403 | url: url('/api/v1/repos/${test_username}/${test_repo}/issues') |
| 404 | header: http.new_header_from_map({ |
| 405 | .authorization: 'Bearer ${bearer_token()}' |
| 406 | .content_type: 'application/x-www-form-urlencoded' |
| 407 | }) |
| 408 | data: 'title=first-issue&body=hello' |
| 409 | ) or { panic(err) } |
| 410 | assert resp.status_code == 200 |
| 411 | issue := json.decode(ApiIssueSummary, resp.body) or { panic(err) } |
| 412 | assert issue.title == 'first-issue' |
| 413 | assert issue.status == 'open' |
| 414 | |
| 415 | listing := http.get(url('/api/v1/repos/${test_username}/${test_repo}/issues')) or { panic(err) } |
| 416 | issues := json.decode([]ApiIssueSummary, listing.body) or { panic(err) } |
| 417 | assert issues.len >= 1 |
| 418 | |
| 419 | single := http.get(url('/api/v1/repos/${test_username}/${test_repo}/issues/${issue.id}')) or { |
| 420 | panic(err) |
| 421 | } |
| 422 | assert single.status_code == 200 |
| 423 | got := json.decode(ApiIssueSummary, single.body) or { panic(err) } |
| 424 | assert got.id == issue.id |
| 425 | } |
| 426 | |
| 427 | fn test_api_v1_repo_issue_not_found() { |
| 428 | resp := http.get(url('/api/v1/repos/${test_username}/${test_repo}/issues/99999')) or { |
| 429 | panic(err) |
| 430 | } |
| 431 | assert resp.status_code == 404 |
| 432 | } |
| 433 | |
| 434 | fn test_api_v1_repo_pulls_empty() { |
| 435 | resp := http.get(url('/api/v1/repos/${test_username}/${test_repo}/pulls')) or { panic(err) } |
| 436 | assert resp.status_code == 200 |
| 437 | prs := json.decode([]ApiPullSummary, resp.body) or { panic(err) } |
| 438 | assert prs.len == 0 |
| 439 | } |
| 440 | |
| 441 | fn test_api_v1_repo_pull_not_found() { |
| 442 | resp := http.get(url('/api/v1/repos/${test_username}/${test_repo}/pulls/1')) or { panic(err) } |
| 443 | assert resp.status_code == 404 |
| 444 | } |
| 445 | |
| 446 | fn test_api_v1_pull_comments_not_found() { |
| 447 | resp := http.get(url('/api/v1/repos/${test_username}/${test_repo}/pulls/1/comments')) or { |
| 448 | panic(err) |
| 449 | } |
| 450 | assert resp.status_code == 404 |
| 451 | } |
| 452 | |
| 453 | fn test_api_v1_issues_count() { |
| 454 | resp := http.fetch( |
| 455 | method: .get |
| 456 | url: url('/api/v1/${test_username}/${test_repo}/issues/count') |
| 457 | cookies: { |
| 458 | 'token': session_cookie() |
| 459 | } |
| 460 | ) or { panic(err) } |
| 461 | assert resp.status_code == 200 |
| 462 | decoded := json.decode(ApiIssueCount, resp.body) or { panic(err) } |
| 463 | assert decoded.success |
| 464 | assert decoded.result >= 1 |
| 465 | } |
| 466 | |
| 467 | fn test_api_v1_issues_count_unauthenticated() { |
| 468 | resp := http.get(url('/api/v1/${test_username}/${test_repo}/issues/count')) or { panic(err) } |
| 469 | assert resp.body.contains('Not found') |
| 470 | } |
| 471 | |
| 472 | fn test_api_v1_branches_count() { |
| 473 | resp := http.fetch( |
| 474 | method: .get |
| 475 | url: url('/api/v1/${test_username}/${test_repo}/branches/count') |
| 476 | cookies: { |
| 477 | 'token': session_cookie() |
| 478 | } |
| 479 | ) or { panic(err) } |
| 480 | assert resp.status_code == 200 |
| 481 | decoded := json.decode(ApiBranchCount, resp.body) or { panic(err) } |
| 482 | assert decoded.success |
| 483 | assert decoded.result == 0 |
| 484 | } |
| 485 | |
| 486 | fn test_api_v1_commits_count() { |
| 487 | resp := http.fetch( |
| 488 | method: .get |
| 489 | url: url('/api/v1/${test_username}/${test_repo}/main/commits/count') |
| 490 | cookies: { |
| 491 | 'token': session_cookie() |
| 492 | } |
| 493 | ) or { panic(err) } |
| 494 | assert resp.status_code == 200 |
| 495 | decoded := json.decode(ApiCommitCount, resp.body) or { panic(err) } |
| 496 | assert decoded.success |
| 497 | assert decoded.result == 0 |
| 498 | } |
| 499 | |
| 500 | fn test_api_v1_repo_star_toggle() { |
| 501 | rid := repo_id() |
| 502 | resp := http.fetch( |
| 503 | method: .post |
| 504 | url: url('/api/v1/repos/${rid}/star') |
| 505 | cookies: { |
| 506 | 'token': session_cookie() |
| 507 | } |
| 508 | ) or { panic(err) } |
| 509 | assert resp.status_code == 200 |
| 510 | first := json.decode(ApiBoolResult, resp.body) or { panic(err) } |
| 511 | assert first.success |
| 512 | assert first.result == true |
| 513 | |
| 514 | resp2 := http.fetch( |
| 515 | method: .post |
| 516 | url: url('/api/v1/repos/${rid}/star') |
| 517 | cookies: { |
| 518 | 'token': session_cookie() |
| 519 | } |
| 520 | ) or { panic(err) } |
| 521 | second := json.decode(ApiBoolResult, resp2.body) or { panic(err) } |
| 522 | assert second.result == false |
| 523 | } |
| 524 | |
| 525 | fn test_api_v1_repo_watch_toggle() { |
| 526 | rid := repo_id() |
| 527 | resp := http.fetch( |
| 528 | method: .post |
| 529 | url: url('/api/v1/repos/${rid}/watch') |
| 530 | cookies: { |
| 531 | 'token': session_cookie() |
| 532 | } |
| 533 | ) or { panic(err) } |
| 534 | assert resp.status_code == 200 |
| 535 | first := json.decode(ApiBoolResult, resp.body) or { panic(err) } |
| 536 | assert first.success |
| 537 | } |
| 538 | |
| 539 | fn test_api_v1_repo_tree_files_requires_branch() { |
| 540 | rid := repo_id() |
| 541 | resp := http.get(url('/api/v1/repos/${rid}/tree/files')) or { panic(err) } |
| 542 | assert resp.body.contains('branch is required') |
| 543 | } |
| 544 | |
| 545 | fn test_api_v1_repo_tree_files_with_branch() { |
| 546 | rid := repo_id() |
| 547 | resp := http.get(url('/api/v1/repos/${rid}/tree/files?branch=main')) or { panic(err) } |
| 548 | assert resp.status_code == 200 |
| 549 | decoded := json.decode(ApiFilesResult, resp.body) or { panic(err) } |
| 550 | assert decoded.success |
| 551 | } |
| 552 | |
| 553 | fn test_api_v1_repo_tree_files_unknown_repo() { |
| 554 | resp := http.get(url('/api/v1/repos/9999999/tree/files?branch=main')) or { panic(err) } |
| 555 | assert resp.body.contains('Not found') |
| 556 | } |
| 557 | |
| 558 | fn test_api_v1_users_avatar_requires_auth() { |
| 559 | resp := http.post_multipart_form(url('/api/v1/users/avatar'), |
| 560 | files: { |
| 561 | 'file': [ |
| 562 | http.FileData{ |
| 563 | filename: 'a.png' |
| 564 | content_type: 'image/png' |
| 565 | data: 'x' |
| 566 | }, |
| 567 | ] |
| 568 | } |
| 569 | ) or { panic(err) } |
| 570 | assert resp.status_code == 404 |
| 571 | } |
| 572 | |
| 573 | fn test_api_v1_ci_status_callback() { |
| 574 | rid := repo_id() |
| 575 | payload := '{"run_id":"123","repo_id":"${rid}","commit_hash":"deadbeef","branch":"main","status":"running"}' |
| 576 | resp := http.fetch( |
| 577 | method: .post |
| 578 | url: url('/api/v1/ci/status') |
| 579 | header: http.new_header(key: .content_type, value: 'application/json') |
| 580 | data: payload |
| 581 | ) or { panic(err) } |
| 582 | assert resp.status_code == 200 |
| 583 | assert resp.body.contains('"success":true') || resp.body.contains('"success": true') |
| 584 | } |
| 585 | |
| 586 | fn test_api_v1_ci_status_callback_rejects_bad_json() { |
| 587 | resp := http.fetch( |
| 588 | method: .post |
| 589 | url: url('/api/v1/ci/status') |
| 590 | header: http.new_header(key: .content_type, value: 'application/json') |
| 591 | data: 'not-json' |
| 592 | ) or { panic(err) } |
| 593 | assert resp.body.contains('Invalid request body') |
| 594 | } |
| 595 | |
| 596 | fn test_api_v1_private_repo_visibility_from_other_user() { |
| 597 | // Sanity check: a second authenticated user can see the public test repo. |
| 598 | resp := http.fetch( |
| 599 | method: .get |
| 600 | url: url('/api/v1/repos/${test_username}/${test_repo}') |
| 601 | cookies: { |
| 602 | 'token': other_session_cookie() |
| 603 | } |
| 604 | ) or { panic(err) } |
| 605 | assert resp.status_code == 200 |
| 606 | } |
| 607 | |