forked from
gleam.run/gleam
⭐️ A friendly language for building type-safe, scalable systems!
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2026 The Gleam contributors
3
4use std::{convert::TryFrom, io::Cursor};
5
6use super::*;
7use serde_json::json;
8
9fn make_response(status: u16, body: Vec<u8>) -> http::Response<Vec<u8>> {
10 http::Response::builder().status(status).body(body).unwrap()
11}
12
13fn make_json_response(status: u16, body: serde_json::Value) -> http::Response<Vec<u8>> {
14 make_response(status, body.to_string().into_bytes())
15}
16
17#[test]
18fn remove_docs_request() {
19 let key = WriteActionCredentials::OAuthAccessToken {
20 access_token: EcoString::from("my-api-key-here"),
21 one_time_password: EcoString::from("test-otp"),
22 };
23 let package = "gleam_experimental_stdlib";
24 let version = "0.8.0";
25
26 let config = Config::new();
27 let request = crate::api_remove_docs_request(package, version, &key, &config).unwrap();
28
29 assert_eq!(request.method(), http::Method::DELETE);
30 assert_eq!(
31 request.uri().path(),
32 "/api/packages/gleam_experimental_stdlib/releases/0.8.0/docs"
33 );
34 assert_eq!(
35 request.headers().get("authorization").unwrap(),
36 "Bearer my-api-key-here"
37 );
38 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
39 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
40}
41
42#[test]
43fn remove_docs_response_success() {
44 let response = make_response(204, vec![]);
45 let result = crate::api_remove_docs_response(response).unwrap();
46 assert_eq!(result, ());
47}
48
49#[test]
50fn revert_release_request() {
51 let key = WriteActionCredentials::OAuthAccessToken {
52 access_token: EcoString::from("my-api-key-here"),
53 one_time_password: EcoString::from("test-otp"),
54 };
55 let package = "gleam_experimental_stdlib";
56 let version = "0.8.0";
57
58 let config = Config::new();
59 let request = crate::api_revert_release_request(package, version, &key, &config).unwrap();
60
61 assert_eq!(request.method(), http::Method::DELETE);
62 assert_eq!(
63 request.uri().path(),
64 "/api/packages/gleam_experimental_stdlib/releases/0.8.0"
65 );
66 assert_eq!(
67 request.headers().get("authorization").unwrap(),
68 "Bearer my-api-key-here"
69 );
70 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
71 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
72}
73
74#[test]
75fn revert_release_response_success() {
76 let response = make_response(204, vec![]);
77 let result = crate::api_revert_release_response(response).unwrap();
78 assert_eq!(result, ());
79}
80
81#[test]
82fn revert_release_response_too_old_error() {
83 let response = make_response(422, r#"{"message":"Validation error(s)","status":422,"errors":{"inserted_at":"can only delete a release up to one hour after publication"}}"#.as_bytes().to_vec());
84 let result = crate::api_revert_release_response(response);
85 assert!(matches!(result, Err(ApiError::LateDeletion)));
86}
87
88#[test]
89fn add_owner_request() {
90 let key = WriteActionCredentials::OAuthAccessToken {
91 access_token: EcoString::from("my-api-key-here"),
92 one_time_password: EcoString::from("test-otp"),
93 };
94 let package = "gleam_experimental_stdlib";
95 let owner = "lpil";
96 let level = OwnerLevel::Maintainer;
97
98 let config = Config::new();
99 let request = crate::api_add_owner_request(package, owner, level, &key, &config);
100
101 assert_eq!(request.method(), http::Method::PUT);
102 assert_eq!(
103 request.uri().path(),
104 "/api/packages/gleam_experimental_stdlib/owners/lpil"
105 );
106 assert_eq!(
107 request.headers().get("authorization").unwrap(),
108 "Bearer my-api-key-here"
109 );
110 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
111 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
112
113 let body: serde_json::Value = serde_json::from_slice(request.body()).unwrap();
114 assert_eq!(body["level"], "maintainer");
115 assert_eq!(body["transfer"], false);
116}
117
118#[test]
119fn add_owner_response_success() {
120 let response = make_response(204, vec![]);
121 let result = crate::api_add_owner_response(response).unwrap();
122 assert_eq!(result, ());
123}
124
125#[test]
126fn transfer_owner_request() {
127 let key = WriteActionCredentials::OAuthAccessToken {
128 access_token: EcoString::from("my-api-key-here"),
129 one_time_password: EcoString::from("test-otp"),
130 };
131 let package = "gleam_experimental_stdlib";
132 let owner = "lpil";
133
134 let config = Config::new();
135 let request = crate::api_transfer_owner_request(package, owner, &key, &config);
136
137 assert_eq!(request.method(), http::Method::PUT);
138 assert_eq!(
139 request.uri().path(),
140 "/api/packages/gleam_experimental_stdlib/owners/lpil"
141 );
142 assert_eq!(
143 request.headers().get("authorization").unwrap(),
144 "Bearer my-api-key-here"
145 );
146 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
147 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
148
149 let body: serde_json::Value = serde_json::from_slice(request.body()).unwrap();
150 assert_eq!(body["level"], "full");
151 assert_eq!(body["transfer"], true);
152}
153
154#[test]
155fn transfer_owner_response_success() {
156 let response = make_response(204, vec![]);
157 let result = crate::api_transfer_owner_response(response).unwrap();
158 assert_eq!(result, ());
159}
160
161#[test]
162fn remove_owner_request() {
163 let key = WriteActionCredentials::OAuthAccessToken {
164 access_token: EcoString::from("my-api-key-here"),
165 one_time_password: EcoString::from("test-otp"),
166 };
167 let package = "gleam_experimental_stdlib";
168 let owner = "lpil";
169
170 let config = Config::new();
171 let request = crate::api_remove_owner_request(package, owner, &key, &config);
172
173 assert_eq!(request.method(), http::Method::DELETE);
174 assert_eq!(
175 request.uri().path(),
176 "/api/packages/gleam_experimental_stdlib/owners/lpil"
177 );
178 assert_eq!(
179 request.headers().get("authorization").unwrap(),
180 "Bearer my-api-key-here"
181 );
182 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
183 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
184}
185
186#[test]
187fn remove_owner_response_success() {
188 let response = make_response(204, vec![]);
189 let result = crate::api_remove_owner_response(response).unwrap();
190 assert_eq!(result, ());
191}
192
193#[test]
194fn remove_key_request() {
195 let name = "some-key-name";
196 let key = WriteActionCredentials::OAuthAccessToken {
197 access_token: EcoString::from("my-api-key-here"),
198 one_time_password: EcoString::from("test-otp"),
199 };
200
201 let config = Config::new();
202 let request = crate::api_remove_api_key_request(name, &key, &config);
203
204 assert_eq!(request.method(), http::Method::DELETE);
205 assert_eq!(request.uri().path(), "/api/keys/some-key-name");
206 assert_eq!(
207 request.headers().get("authorization").unwrap(),
208 "Bearer my-api-key-here"
209 );
210 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
211 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
212}
213
214#[test]
215fn remove_key_response_success_204() {
216 let response = make_response(204, vec![]);
217 let result = crate::api_remove_api_key_response(response).unwrap();
218 assert_eq!(result, ());
219}
220
221#[test]
222fn remove_key_response_success_200() {
223 let response = make_response(200, vec![]);
224 let result = crate::api_remove_api_key_response(response).unwrap();
225 assert_eq!(result, ());
226}
227
228#[test]
229fn remove_docs_response_not_found() {
230 let response = make_response(404, vec![]);
231 let result = crate::api_remove_docs_response(response).unwrap_err();
232
233 match result {
234 ApiError::NotFound => (),
235 result => panic!("expected ApiError::NotFound got {result:?}"),
236 }
237}
238
239#[test]
240fn remove_docs_response_rate_limited() {
241 let response = make_response(429, vec![]);
242 let result = crate::api_remove_docs_response(response).unwrap_err();
243
244 match result {
245 ApiError::RateLimited => (),
246 result => panic!("expected ApiError::RateLimited got {result:?}"),
247 }
248}
249
250#[test]
251fn remove_docs_response_invalid_key() {
252 let resp_body = json!({
253 "message": "invalid API key",
254 "status": 401,
255 });
256 let response = make_json_response(401, resp_body);
257 let result = crate::api_remove_docs_response(response).unwrap_err();
258
259 match result {
260 ApiError::InvalidCredentials => (),
261 result => panic!("expected ApiError::InvalidApiKey got {result:?}"),
262 }
263}
264
265#[test]
266fn remove_docs_response_forbidden() {
267 let resp_body = json!({
268 "message": "account is not authorized for this action",
269 "status": 403,
270 });
271 let response = make_json_response(403, resp_body);
272 let result = crate::api_remove_docs_response(response).unwrap_err();
273
274 match result {
275 ApiError::Forbidden => (),
276 result => panic!("expected ApiError::Forbidden got {result:?}"),
277 }
278}
279
280#[test]
281fn remove_docs_bad_package_name() {
282 let key = WriteActionCredentials::OAuthAccessToken {
283 access_token: EcoString::from("my-api-key-here"),
284 one_time_password: EcoString::from("test-otp"),
285 };
286 let package = "not valid";
287 let version = "1.2.0";
288
289 let config = Config::new();
290
291 match crate::api_remove_docs_request(package, version, &key, &config).unwrap_err() {
292 ApiError::InvalidPackageNameFormat(p) if p == package => (),
293 result => panic!("expected Err(ApiError::BadPackage), got {result:?}"),
294 }
295}
296
297#[test]
298fn publish_docs_request() {
299 let key = WriteActionCredentials::OAuthAccessToken {
300 access_token: EcoString::from("my-api-key-here"),
301 one_time_password: EcoString::from("test-otp"),
302 };
303 let package = "gleam_experimental_stdlib_123";
304 let version = "0.8.0";
305 let tarball = std::include_bytes!("../test/example.tar.gz").to_vec();
306
307 let config = Config::new();
308 let request =
309 crate::api_publish_docs_request(package, version, tarball.clone(), &key, &config).unwrap();
310
311 assert_eq!(request.method(), http::Method::POST);
312 assert_eq!(
313 request.uri().path(),
314 "/api/packages/gleam_experimental_stdlib_123/releases/0.8.0/docs"
315 );
316 assert_eq!(
317 request.headers().get("authorization").unwrap(),
318 &"Bearer my-api-key-here"
319 );
320 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
321 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
322 assert_eq!(
323 request.headers().get("content-type").unwrap(),
324 "application/x-tar"
325 );
326 assert_eq!(request.headers().get("content-encoding").unwrap(), "x-gzip");
327 assert_eq!(request.body(), &tarball);
328}
329
330#[test]
331fn publish_docs_response_success() {
332 let response = make_response(201, vec![]);
333 let result = crate::api_publish_docs_response(response);
334
335 match result {
336 Ok(()) => (),
337 result => panic!("expected Ok(()), got {result:?}"),
338 }
339}
340
341#[test]
342fn publish_docs_bad_package_name() {
343 let key = WriteActionCredentials::OAuthAccessToken {
344 access_token: EcoString::from("my-api-key-here"),
345 one_time_password: EcoString::from("test-otp"),
346 };
347 let package = "not valid";
348 let version = "1.2.0";
349 let tarball = std::include_bytes!("../test/example.tar.gz").to_vec();
350
351 let config = Config::new();
352
353 match crate::api_publish_docs_request(package, version, tarball, &key, &config).unwrap_err() {
354 ApiError::InvalidPackageNameFormat(p) if p == package => (),
355 result => panic!("expected Err(ApiError::BadPackage), got {result:?}"),
356 }
357}
358
359#[test]
360fn publish_docs_bad_package_version() {
361 let key = WriteActionCredentials::OAuthAccessToken {
362 access_token: EcoString::from("my-api-key-here"),
363 one_time_password: EcoString::from("test-otp"),
364 };
365 let package = "name";
366 let version = "invalid version";
367 let tarball = std::include_bytes!("../test/example.tar.gz").to_vec();
368
369 let config = Config::new();
370
371 match crate::api_publish_docs_request(package, version, tarball, &key, &config).unwrap_err() {
372 ApiError::InvalidVersionFormat(v) if v == version => (),
373 result => panic!("expected ApiError::BadPackage, got {result:?}"),
374 }
375}
376
377#[test]
378fn publish_docs_response_not_found() {
379 let response = make_response(404, vec![]);
380 let result = crate::api_publish_docs_response(response);
381
382 match result {
383 Err(ApiError::NotFound) => (),
384 result => panic!("expected ApiError::NotFound, got {result:?}"),
385 }
386}
387
388#[test]
389fn publish_docs_response_rate_limit() {
390 let response = make_response(429, vec![]);
391 let result = crate::api_publish_docs_response(response);
392
393 match result {
394 Err(ApiError::RateLimited) => (),
395 result => panic!("expected ApiError::RateLimited, got {result:?}"),
396 }
397}
398
399#[test]
400fn publish_docs_response_invalid_api_key() {
401 let resp_body = json!({
402 "message": "invalid API key",
403 "status": 401,
404 });
405 let response = make_json_response(401, resp_body);
406 let result = crate::api_publish_docs_response(response);
407
408 match result {
409 Err(ApiError::InvalidCredentials) => (),
410 result => panic!("expected Err(ApiError::InvalidApiKey), got {result:?}"),
411 }
412}
413
414#[test]
415fn publish_docs_response_incorrect_totp_key() {
416 let resp_body = json!({
417 "status": 401,
418 });
419 let mut response = make_json_response(401, resp_body);
420 response.headers_mut().insert(
421 "www-authenticate",
422 "Bearer realm=\"hex\", error=\"invalid_totp\""
423 .try_into()
424 .expect("Header value"),
425 );
426 let result = crate::api_publish_docs_response(response);
427
428 match result {
429 Err(ApiError::IncorrectOneTimePassword) => (),
430 result => panic!("expected Err(ApiError::IncorrectOneTimePassword), got {result:?}"),
431 }
432}
433
434#[test]
435fn publish_docs_response_forbidden() {
436 let resp_body = json!({
437 "message": "account is not authorized for this action",
438 "status": 403,
439 });
440 let response = make_json_response(403, resp_body);
441 let result = crate::api_publish_docs_response(response);
442
443 match result {
444 Err(ApiError::Forbidden) => (),
445 result => panic!("expected Err(ApiError::Forbidden), got {result:?}"),
446 }
447}
448
449fn expected_package_exfmt() -> Package {
450 Package {
451 name: "exfmt".to_string(),
452 repository: "hexpm".to_string(),
453 releases: vec![
454 Release {
455 version: Version::try_from("0.0.0").unwrap(),
456 requirements: [].into(),
457 retirement_status: None,
458 outer_checksum: vec![
459 82, 48, 191, 145, 92, 172, 0, 108, 238, 71, 57, 23, 101, 177, 161, 83, 91, 182,
460 18, 232, 249, 225, 29, 12, 246, 5, 215, 165, 32, 57, 179, 110,
461 ],
462 meta: (),
463 },
464 Release {
465 version: Version::try_from("0.1.0").unwrap(),
466 requirements: [].into(),
467 retirement_status: None,
468 outer_checksum: vec![
469 111, 246, 240, 176, 118, 229, 12, 15, 164, 61, 186, 3, 89, 106, 153, 225, 247,
470 52, 245, 8, 216, 139, 21, 232, 200, 16, 214, 59, 241, 188, 9, 6,
471 ],
472 meta: (),
473 },
474 Release {
475 version: Version::try_from("0.2.0").unwrap(),
476 requirements: [].into(),
477 retirement_status: None,
478 outer_checksum: vec![
479 149, 9, 192, 229, 84, 162, 110, 207, 161, 43, 31, 0, 126, 168, 14, 243, 31, 43,
480 195, 238, 100, 91, 78, 100, 213, 181, 101, 154, 106, 168, 170, 107,
481 ],
482 meta: (),
483 },
484 Release {
485 version: Version::try_from("0.2.1").unwrap(),
486 requirements: [].into(),
487 retirement_status: None,
488 outer_checksum: vec![
489 157, 229, 28, 212, 92, 249, 14, 240, 235, 104, 31, 12, 160, 199, 83, 195, 154,
490 105, 222, 37, 221, 80, 181, 183, 113, 240, 234, 107, 144, 85, 255, 65,
491 ],
492 meta: (),
493 },
494 Release {
495 version: Version::try_from("0.2.2").unwrap(),
496 requirements: [].into(),
497 retirement_status: None,
498 outer_checksum: vec![
499 112, 250, 133, 189, 183, 192, 54, 218, 115, 55, 216, 97, 204, 201, 191, 168,
500 250, 133, 138, 252, 202, 240, 74, 197, 228, 235, 81, 18, 241, 7, 155, 38,
501 ],
502 meta: (),
503 },
504 Release {
505 version: Version::try_from("0.2.3").unwrap(),
506 requirements: [].into(),
507 retirement_status: None,
508 outer_checksum: vec![
509 131, 20, 29, 160, 171, 124, 7, 125, 210, 88, 17, 189, 199, 49, 191, 190, 14,
510 162, 38, 247, 52, 176, 189, 17, 7, 188, 151, 152, 24, 64, 170, 29,
511 ],
512 meta: (),
513 },
514 Release {
515 version: Version::try_from("0.2.4").unwrap(),
516 requirements: [].into(),
517 retirement_status: None,
518 outer_checksum: vec![
519 109, 162, 185, 169, 26, 4, 62, 60, 167, 54, 182, 161, 140, 197, 75, 113, 183,
520 117, 247, 201, 218, 228, 14, 160, 115, 157, 196, 51, 108, 16, 96, 217,
521 ],
522 meta: (),
523 },
524 Release {
525 version: Version::try_from("0.3.0").unwrap(),
526 requirements: [].into(),
527 retirement_status: None,
528 outer_checksum: vec![
529 97, 50, 95, 212, 242, 59, 245, 177, 140, 78, 79, 180, 108, 174, 119, 176, 24,
530 80, 218, 152, 178, 227, 152, 242, 32, 126, 72, 67, 222, 0, 173, 170,
531 ],
532 meta: (),
533 },
534 Release {
535 version: Version::try_from("0.4.0").unwrap(),
536 requirements: [].into(),
537 retirement_status: None,
538 outer_checksum: vec![
539 246, 178, 237, 214, 217, 158, 143, 52, 130, 186, 64, 50, 94, 175, 161, 81, 68,
540 186, 4, 73, 53, 226, 235, 144, 209, 84, 231, 136, 165, 119, 122, 126,
541 ],
542 meta: (),
543 },
544 Release {
545 version: Version::try_from("0.5.0").unwrap(),
546 requirements: [].into(),
547 retirement_status: None,
548 outer_checksum: vec![
549 151, 86, 157, 218, 218, 131, 240, 119, 198, 216, 202, 240, 65, 17, 57, 228, 84,
550 252, 59, 207, 246, 49, 22, 21, 52, 47, 51, 139, 190, 9, 95, 109,
551 ],
552 meta: (),
553 },
554 ],
555 }
556}
557
558#[test]
559fn get_package_request() {
560 let config = Config::new();
561 let request = crate::repository_v2_get_package_request("exfmt", None, &config);
562
563 assert_eq!(request.method(), http::Method::GET);
564 assert_eq!(request.uri().path(), "/packages/exfmt");
565 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
566}
567
568#[test]
569fn get_package_response_ok() {
570 let response_body = std::include_bytes!("../test/package_exfmt");
571 let response = make_response(200, response_body.to_vec());
572
573 let package = crate::repository_v2_get_package_response(
574 response,
575 std::include_bytes!("../test/public_key"),
576 )
577 .unwrap();
578
579 assert_eq!(expected_package_exfmt(), package);
580}
581
582#[test]
583fn get_package_response_not_found() {
584 let response = make_response(404, vec![]);
585 let error = crate::repository_v2_get_package_response(
586 response,
587 std::include_bytes!("../test/public_key"),
588 )
589 .unwrap_err();
590
591 assert!(error.is_not_found());
592}
593
594#[test]
595fn get_package_response_unauthorized() {
596 let response = make_response(401, vec![]);
597 let error = crate::repository_v2_get_package_response(
598 response,
599 std::include_bytes!("../test/public_key"),
600 )
601 .unwrap_err();
602
603 assert_eq!(error.to_string(), "Invalid authentication credentials");
604}
605
606#[test]
607fn get_package_from_bytes_ok() {
608 let response_body = std::include_bytes!("../test/package_exfmt");
609 let mut uncompressed = Vec::new();
610 let mut decoder = GzDecoder::new(Cursor::new(response_body));
611 let _ = decoder
612 .read_to_end(&mut uncompressed)
613 .expect("failed to decompress body");
614
615 let package = crate::repository_v2_package_parse_body(
616 &uncompressed,
617 std::include_bytes!("../test/public_key"),
618 )
619 .expect("package failed to parse");
620
621 assert_eq!(expected_package_exfmt(), package);
622}
623
624#[test]
625fn get_package_from_bytes_malformed() {
626 // public key should not be a valid protobuf and should therefore fail
627 let bytes = std::include_bytes!("../test/public_key").to_vec();
628 let package_error = crate::repository_v2_package_parse_body(&bytes, &bytes)
629 .expect_err("parsing failed to fail");
630
631 assert!(package_error.is_invalid_protobuf());
632}
633
634#[test]
635fn get_repository_versions_request() {
636 let config = Config::new();
637 let request = crate::repository_v2_get_versions_request(None, &config);
638
639 assert_eq!(request.method(), http::Method::GET);
640 assert_eq!(request.uri().path(), "/versions");
641 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
642}
643
644#[test]
645fn get_repository_versions_response_ok() {
646 let response_body = std::include_bytes!("../test/versions");
647 let response = make_response(200, response_body.to_vec());
648
649 let versions = crate::repository_v2_get_versions_response(
650 response,
651 std::include_bytes!("../test/public_key"),
652 );
653
654 assert_eq!(
655 &vec![
656 Version::parse("0.0.0").unwrap(),
657 Version::parse("0.1.0").unwrap(),
658 Version::parse("0.2.0").unwrap(),
659 Version::parse("0.2.1").unwrap(),
660 Version::parse("0.2.2").unwrap(),
661 Version::parse("0.2.3").unwrap(),
662 Version::parse("0.2.4").unwrap(),
663 Version::parse("0.3.0").unwrap(),
664 Version::parse("0.4.0").unwrap(),
665 Version::parse("0.5.0").unwrap(),
666 ],
667 versions.unwrap().get("exfmt").unwrap(),
668 );
669}
670
671#[test]
672fn get_repository_versions_from_bytes_ok() {
673 let response_body = std::include_bytes!("../test/versions");
674 let mut uncompressed = Vec::new();
675 let mut decoder = GzDecoder::new(Cursor::new(response_body));
676 let _ = decoder
677 .read_to_end(&mut uncompressed)
678 .expect("failed to decompress body");
679
680 let versions = crate::repository_v2_get_versions_body(
681 &uncompressed,
682 std::include_bytes!("../test/public_key"),
683 )
684 .expect("versions failed to parse");
685
686 assert_eq!(
687 &vec![
688 Version::parse("0.0.0").unwrap(),
689 Version::parse("0.1.0").unwrap(),
690 Version::parse("0.2.0").unwrap(),
691 Version::parse("0.2.1").unwrap(),
692 Version::parse("0.2.2").unwrap(),
693 Version::parse("0.2.3").unwrap(),
694 Version::parse("0.2.4").unwrap(),
695 Version::parse("0.3.0").unwrap(),
696 Version::parse("0.4.0").unwrap(),
697 Version::parse("0.5.0").unwrap(),
698 ],
699 versions.get("exfmt").unwrap(),
700 );
701}
702
703#[test]
704fn get_repository_versions_from_bytes_malformed() {
705 // public key should not be a valid protobuf and should therefore fail
706 let bytes = std::include_bytes!("../test/public_key").to_vec();
707 let versions_error =
708 crate::repository_v2_get_versions_body(&bytes, &bytes).expect_err("parsing failed to fail");
709
710 assert!(versions_error.is_invalid_protobuf());
711}
712
713#[test]
714fn get_repository_tarball_request() {
715 let config = Config::new();
716 let request =
717 crate::repository_get_package_tarball_request("gleam_stdlib", "0.14.0", None, &config);
718
719 assert_eq!(request.method(), http::Method::GET);
720 assert_eq!(request.uri().path(), "/tarballs/gleam_stdlib-0.14.0.tar");
721 assert_eq!(
722 request.headers().get("accept").unwrap(),
723 "application/x-tar"
724 );
725}
726
727#[test]
728fn get_repository_tarball_response_ok() {
729 let tarball_bytes = std::include_bytes!("../test/gleam_stdlib-0.14.0.tar");
730 let checksum =
731 base16::decode("9107f6a859cb96945ad9a099085db028ca2bebb3c8ea42eec227b51c614cc2e0").unwrap();
732
733 let response = make_response(200, tarball_bytes.to_vec());
734 let downloaded = crate::repository_get_package_tarball_response(response, &checksum).unwrap();
735
736 assert_eq!(&downloaded, tarball_bytes);
737}
738
739#[test]
740fn get_repository_tarball_response_bad_checksum() {
741 let tarball_bytes = std::include_bytes!("../test/gleam_stdlib-0.14.0.tar");
742 let checksum = vec![1, 2, 3, 4, 5];
743
744 let response = make_response(200, tarball_bytes.to_vec());
745 let err = crate::repository_get_package_tarball_response(response, &checksum).unwrap_err();
746
747 assert_eq!(
748 err.to_string(),
749 "The downloaded data did not have the expected checksum"
750 );
751}
752
753#[test]
754fn get_repository_tarball_response_not_found() {
755 let checksum = vec![1, 2, 3, 4, 5];
756
757 let response = make_response(404, vec![]);
758 let err = crate::repository_get_package_tarball_response(response, &checksum).unwrap_err();
759
760 assert_eq!(err.to_string(), "Resource was not found");
761}
762
763#[test]
764fn get_repository_tarball_response_rate_limited() {
765 let checksum = vec![1, 2, 3, 4, 5];
766
767 let response = make_response(429, vec![]);
768 let err = crate::repository_get_package_tarball_response(response, &checksum).unwrap_err();
769
770 assert_eq!(
771 err.to_string(),
772 "The rate limit for the Hex API has been exceeded"
773 );
774}
775
776#[test]
777fn publish_package_request() {
778 let key = WriteActionCredentials::ApiKey(EcoString::from("my-api-key-here"));
779 let tarball = std::include_bytes!("../test/example.tar.gz").to_vec();
780
781 let config = Config::new();
782 let request = crate::api_publish_package_request(tarball.clone(), &key, &config, false);
783
784 assert_eq!(request.method(), http::Method::POST);
785 assert_eq!(
786 request.uri().path_and_query().unwrap(),
787 "/api/publish?replace=false"
788 );
789 assert_eq!(
790 request.headers().get("authorization").unwrap(),
791 "my-api-key-here"
792 );
793 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
794 assert_eq!(
795 request.headers().get("content-type").unwrap(),
796 "application/x-tar"
797 );
798 assert_eq!(request.body(), &tarball);
799}
800
801#[test]
802fn publish_package_request_replace() {
803 let key = WriteActionCredentials::OAuthAccessToken {
804 access_token: EcoString::from("my-api-key-here"),
805 one_time_password: EcoString::from("test-otp"),
806 };
807 let tarball = std::include_bytes!("../test/example.tar.gz").to_vec();
808
809 let config = Config::new();
810 let request = crate::api_publish_package_request(tarball.clone(), &key, &config, true);
811
812 assert_eq!(
813 request.uri().path_and_query().unwrap(),
814 "/api/publish?replace=true"
815 );
816 assert_eq!(request.headers().get("x-hex-otp").unwrap(), "test-otp");
817}
818
819#[test]
820fn publish_package_response_success() {
821 let response = make_response(201, vec![]);
822 let result = crate::api_publish_package_response(response);
823
824 match result {
825 Ok(()) => (),
826 result => panic!("expected Ok(()), got {result:?}"),
827 }
828}
829
830#[test]
831fn modify_package_late() {
832 let resp_body = json!({
833 "errors": {"inserted_at": "can only modify a release up to one hour after publication"},
834 "message": "Validation error(s)",
835 "status": 422,
836 });
837 let response = make_json_response(422, resp_body);
838 let result = crate::api_publish_package_response(response);
839
840 match result {
841 Err(ApiError::LateModification) => (),
842 result => panic!("expected Err(ApiError::LateModification), got {result:?}"),
843 }
844}
845
846#[test]
847fn not_replacing() {
848 let resp_body = json!({
849 "errors": {"inserted_at": "must include the --replace flag to update an existing release"},
850 "message": "Validation error(s)",
851 "status": 422,
852 });
853 let response = make_json_response(422, resp_body);
854 let result = crate::api_publish_package_response(response);
855
856 match result {
857 Err(ApiError::NotReplacing) => (),
858 result => panic!("expected Err(ApiError::NotReplacing), got {result:?}"),
859 }
860}
861
862#[test]
863fn unknown_error_422() {
864 let resp_body = json!({
865 "errors": {"tar": "file too big: metadata.config"},
866 "message": "Validation error(s)",
867 "status": 422,
868 });
869 let response = make_json_response(422, resp_body);
870 let result = crate::api_publish_package_response(response);
871
872 match result {
873 Err(ApiError::UnexpectedResponse(status, body)) => {
874 assert_eq!(status, http::StatusCode::UNPROCESSABLE_ENTITY);
875 assert!(
876 body.contains("file too big: metadata.config"),
877 "expected file too big string copied from hexpm error, got: {body}"
878 );
879 }
880 result => panic!("expected Err(ApiError::UnexpectedResponse), got {result:?}"),
881 }
882}
883
884#[test]
885fn get_package_release_request() {
886 let config = Config::new();
887 let request = crate::api_get_package_release_request("clint", "0.0.1", None, &config);
888
889 assert_eq!(request.method(), http::Method::GET);
890 assert_eq!(request.uri().path(), "/api/packages/clint/releases/0.0.1");
891 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
892}
893
894#[test]
895fn get_package_release_response_not_found() {
896 let response = make_response(404, vec![]);
897 let error = crate::api_get_package_release_response(response).unwrap_err();
898
899 assert!(error.is_not_found());
900}
901
902#[test]
903fn get_package_release_response_ok() {
904 let resp_body = json!({
905 "version": "0.0.1",
906 "checksum": "41C6781B5F4B986BCE14C3578D39C497BCB8427F1D36D8CDE5FCAA6E03CAE2B1",
907 "requirements": {
908 "plug": {
909 "requirement": "~>0.11.0",
910 "optional": false,
911 "app": "plug"
912 },
913 "cowboy": {
914 "requirement": "~>1.0.0",
915 "optional": false,
916 "app": "cowboy"
917 }
918 },
919 "meta": {
920 "app": "clint",
921 "build_tools": ["mix"]
922 }
923 });
924 let response = make_json_response(200, resp_body);
925 let resp = crate::api_get_package_release_response(response).unwrap();
926
927 assert_eq!(
928 resp,
929 Release {
930 version: Version::new(0, 0, 1),
931 requirements: [
932 (
933 "plug".into(),
934 Dependency {
935 requirement: Range::new("~>0.11.0".into()).unwrap(),
936 optional: false,
937 app: Some("plug".into()),
938 repository: None
939 }
940 ),
941 (
942 "cowboy".into(),
943 Dependency {
944 requirement: Range::new("~>1.0.0".into()).unwrap(),
945 optional: false,
946 app: Some("cowboy".into()),
947 repository: None
948 }
949 )
950 ]
951 .into(),
952 retirement_status: None,
953 outer_checksum: vec![
954 65, 198, 120, 27, 95, 75, 152, 107, 206, 20, 195, 87, 141, 57, 196, 151, 188, 184,
955 66, 127, 29, 54, 216, 205, 229, 252, 170, 110, 3, 202, 226, 177
956 ],
957 meta: ReleaseMeta {
958 app: "clint".into(),
959 build_tools: vec!["mix".into()]
960 }
961 }
962 )
963}
964
965#[test]
966fn make_request_base_trailing_slash_is_optional() {
967 let slash = http::Uri::from_static("http://host/path/");
968 let no_slash = http::Uri::from_static("http://host/path");
969 let suffix = "suffix";
970 let expect = "/path/suffix";
971
972 let slash = make_request(slash, http::Method::GET, suffix);
973 assert_eq!(slash.uri_ref().unwrap().path(), expect);
974
975 let no_slash = make_request(no_slash, http::Method::GET, suffix);
976 assert_eq!(no_slash.uri_ref().unwrap().path(), expect);
977}
978
979#[test]
980fn oauth_device_authorisation_request() {
981 let client_id = "test-client-id";
982
983 let config = Config::new();
984 let request = crate::oauth_device_authorisation_request(client_id, "My client", &config);
985
986 assert_eq!(request.method(), http::Method::POST);
987 assert_eq!(request.uri().path(), "/api/oauth/device_authorization");
988 assert_eq!(
989 request.headers().get("content-type").unwrap(),
990 "application/json"
991 );
992 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
993
994 let body: serde_json::Value = serde_json::from_slice(request.body()).unwrap();
995 assert_eq!(body["client_id"], "test-client-id");
996 assert_eq!(body["scope"], "api:write");
997 assert_eq!(body["name"], "My client");
998}
999
1000#[test]
1001fn oauth_device_authorisation_response_success() {
1002 let client_id = "test-client-id".to_string();
1003 let resp_body = json!({
1004 "device_code": "device-code-123",
1005 "user_code": "USER-CODE",
1006 "verification_uri": "https://hex.pm/oauth/device",
1007 "interval": 5
1008 });
1009
1010 let response = make_json_response(200, resp_body);
1011 let auth = crate::oauth_device_authorisation_response(client_id, response).unwrap();
1012
1013 assert_eq!(auth.user_code, "USER-CODE");
1014 assert_eq!(auth.verification_uri, "https://hex.pm/oauth/device");
1015}
1016
1017#[test]
1018fn oauth_device_authorisation_response_success_with_complete_uri() {
1019 let client_id = "test-client-id".to_string();
1020 let resp_body = json!({
1021 "device_code": "device-code-123",
1022 "user_code": "USER-CODE",
1023 "verification_uri": "https://hex.pm/oauth/device",
1024 "verification_uri_complete": "https://hex.pm/oauth/device?user_code=USER-CODE",
1025 "interval": 5
1026 });
1027
1028 let response = make_json_response(200, resp_body);
1029 let auth = crate::oauth_device_authorisation_response(client_id, response).unwrap();
1030
1031 assert_eq!(auth.user_code, "USER-CODE");
1032 // When verification_uri_complete is present, it should be used instead
1033 assert_eq!(
1034 auth.verification_uri,
1035 "https://hex.pm/oauth/device?user_code=USER-CODE"
1036 );
1037}
1038
1039#[test]
1040fn oauth_device_authorisation_response_default_interval() {
1041 let client_id = "test-client-id".to_string();
1042 // Response without "interval" field - should use default of 5 seconds
1043 let resp_body = json!({
1044 "device_code": "device-code-123",
1045 "user_code": "USER-CODE",
1046 "verification_uri": "https://hex.pm/oauth/device"
1047 });
1048
1049 let response = make_json_response(200, resp_body);
1050 let auth = crate::oauth_device_authorisation_response(client_id, response).unwrap();
1051
1052 assert_eq!(auth.user_code, "USER-CODE");
1053 assert_eq!(auth.verification_uri, "https://hex.pm/oauth/device");
1054}
1055
1056#[test]
1057fn oauth_device_authorisation_response_rate_limited() {
1058 let client_id = "test-client-id".to_string();
1059 let response = make_response(429, vec![]);
1060 let result = crate::oauth_device_authorisation_response(client_id, response).unwrap_err();
1061
1062 match result {
1063 ApiError::RateLimited => (),
1064 result => panic!("expected RateLimited, got {:?}", result),
1065 }
1066}
1067
1068#[test]
1069fn oauth_device_authorisation_response_unexpected_status() {
1070 let client_id = "test-client-id".to_string();
1071 let resp_body = json!({
1072 "message": "Internal server error",
1073 "status": 500,
1074 });
1075 let response = make_json_response(500, resp_body);
1076 let result = crate::oauth_device_authorisation_response(client_id, response).unwrap_err();
1077
1078 match result {
1079 ApiError::UnexpectedResponse(status, _) => {
1080 assert_eq!(status, http::StatusCode::INTERNAL_SERVER_ERROR);
1081 }
1082 result => panic!("expected UnexpectedResponse, got {:?}", result),
1083 }
1084}
1085
1086fn make_device_authorisation() -> crate::OAuthDeviceAuthorisation {
1087 let client_id = "test-client-id".to_string();
1088 let resp_body = json!({
1089 "device_code": "device-code-123",
1090 "user_code": "USER-CODE",
1091 "verification_uri": "https://hex.pm/oauth/device",
1092 "interval": 5
1093 });
1094 let response = make_json_response(200, resp_body);
1095 crate::oauth_device_authorisation_response(client_id, response).unwrap()
1096}
1097
1098#[test]
1099fn poll_token_request() {
1100 let auth = make_device_authorisation();
1101 let config = Config::new();
1102 let request = auth.poll_token_request(&config);
1103
1104 assert_eq!(request.method(), http::Method::POST);
1105 assert_eq!(request.uri().path(), "/api/oauth/token");
1106 assert_eq!(
1107 request.headers().get("content-type").unwrap(),
1108 "application/json"
1109 );
1110 assert_eq!(request.headers().get("accept").unwrap(), "application/json");
1111
1112 let body: serde_json::Value = serde_json::from_slice(request.body()).unwrap();
1113 assert_eq!(
1114 body["grant_type"],
1115 "urn:ietf:params:oauth:grant-type:device_code"
1116 );
1117 assert_eq!(body["client_id"], "test-client-id");
1118 assert_eq!(body["device_code"], "device-code-123");
1119}
1120
1121#[test]
1122fn poll_token_response_success() {
1123 let mut auth = make_device_authorisation();
1124 let resp_body = json!({
1125 "access_token": "access-token-abc",
1126 "refresh_token": "refresh-token-xyz"
1127 });
1128 let response = make_json_response(200, resp_body);
1129
1130 let result = auth.poll_token_response(response).unwrap();
1131
1132 match result {
1133 crate::PollStep::Done(tokens) => {
1134 assert_eq!(tokens.access_token, "access-token-abc");
1135 assert_eq!(tokens.refresh_token, "refresh-token-xyz");
1136 }
1137 result => panic!("expected PollStep::Done, got {:?}", result),
1138 }
1139}
1140
1141#[test]
1142fn poll_token_response_authorization_pending() {
1143 let mut auth = make_device_authorisation();
1144 let resp_body = json!({
1145 "error": "authorization_pending"
1146 });
1147 let response = make_json_response(200, resp_body);
1148
1149 let result = auth.poll_token_response(response).unwrap();
1150
1151 match result {
1152 crate::PollStep::SleepThenPollAgain(duration) => {
1153 assert_eq!(duration, std::time::Duration::from_secs(5));
1154 }
1155 result => panic!("expected PollStep::SleepThenPollAgain, got {:?}", result),
1156 }
1157}
1158
1159#[test]
1160fn poll_token_response_slow_down() {
1161 let mut auth = make_device_authorisation();
1162 let resp_body = json!({
1163 "error": "slow_down"
1164 });
1165 let response = make_json_response(200, resp_body);
1166
1167 let result = auth.poll_token_response(response).unwrap();
1168
1169 match result {
1170 crate::PollStep::SleepThenPollAgain(duration) => {
1171 // Interval should be doubled from 5 to 10 seconds
1172 assert_eq!(duration, std::time::Duration::from_secs(10));
1173 }
1174 result => panic!("expected PollStep::SleepThenPollAgain, got {:?}", result),
1175 }
1176}
1177
1178#[test]
1179fn poll_token_response_unexpected_status() {
1180 let mut auth = make_device_authorisation();
1181 let resp_body = json!({
1182 "message": "Internal server error",
1183 "status": 500,
1184 });
1185 let response = make_json_response(500, resp_body);
1186
1187 let result = auth.poll_token_response(response).unwrap_err();
1188
1189 match result {
1190 ApiError::UnexpectedResponse(status, _) => {
1191 assert_eq!(status, http::StatusCode::INTERNAL_SERVER_ERROR);
1192 }
1193 result => panic!("expected UnexpectedResponse, got {:?}", result),
1194 }
1195}