1mod client;
50mod error;
51mod http;
52mod types;
53
54#[cfg(feature = "connect")]
55mod connect;
56
57pub use client::{Client, ClientOptions, CreateSessionOptions, Profiles, Proxy, Sessions};
58pub use error::{ApiErrorBody, SolariError, SolariErrorCode};
59pub use types::{
60 Cookie, LocalStorageEntry, Profile, ProfileSaveResult, ProxyCountries, ProxyRequest, ProxySpec,
61 ProxyTier, ReplayUrl, ResolvedProxyConfig, SameSite, Session, SessionView, SolariRegion,
62 StorageOrigin, StorageState,
63};
64
65#[cfg(feature = "connect")]
66pub use connect::{connect, connect_endpoint, ConnectedBrowser};
67
68#[cfg(test)]
69mod tests {
70 use super::client::{derive_cdp_from_ws, iso8601_utc};
71 use super::*;
72 use std::sync::{Arc, Mutex};
73 use tokio::io::{AsyncReadExt, AsyncWriteExt};
74 use tokio::net::TcpListener;
75
76 async fn bind_mock() -> (TcpListener, u16) {
82 let l = TcpListener::bind("127.0.0.1:0").await.unwrap();
83 let port = l.local_addr().unwrap().port();
84 (l, port)
85 }
86
87 fn serve(listener: TcpListener, responses: Vec<(u16, String)>) -> Arc<Mutex<Vec<String>>> {
89 let captured: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
90 let cap = captured.clone();
91 tokio::spawn(async move {
92 for (status, body) in responses {
93 let Ok((mut sock, _)) = listener.accept().await else {
94 return;
95 };
96 let mut buf = Vec::new();
98 let mut tmp = [0u8; 1024];
99 while let Ok(n) = sock.read(&mut tmp).await {
100 if n == 0 {
101 break; }
103 buf.extend_from_slice(&tmp[..n]);
104 let Some(pos) = find_subslice(&buf, b"\r\n\r\n") else {
105 continue; };
107 let head = String::from_utf8_lossy(&buf[..pos]).to_string();
108 let clen = content_length(&head).unwrap_or(0);
109 let body_start = pos + 4;
110 while buf.len() < body_start + clen {
111 match sock.read(&mut tmp).await {
112 Ok(0) | Err(_) => break,
113 Ok(n) => buf.extend_from_slice(&tmp[..n]),
114 }
115 }
116 break;
117 }
118 cap.lock()
119 .unwrap()
120 .push(String::from_utf8_lossy(&buf).to_string());
121
122 let reason = match status {
123 200 => "OK",
124 201 => "Created",
125 204 => "No Content",
126 400 => "Bad Request",
127 402 => "Payment Required",
128 404 => "Not Found",
129 429 => "Too Many Requests",
130 503 => "Service Unavailable",
131 _ => "Status",
132 };
133 let resp = format!(
134 "HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
135 body.len(),
136 body
137 );
138 let _ = sock.write_all(resp.as_bytes()).await;
139 let _ = sock.flush().await;
140 }
141 });
142 captured
143 }
144
145 fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
146 haystack.windows(needle.len()).position(|w| w == needle)
147 }
148
149 fn content_length(head: &str) -> Option<usize> {
150 for line in head.lines() {
151 if let Some(v) = line.to_lowercase().strip_prefix("content-length:") {
152 return v.trim().parse().ok();
153 }
154 }
155 None
156 }
157
158 fn split_req(raw: &str) -> (String, String) {
159 match raw.split_once("\r\n\r\n") {
160 Some((h, b)) => (h.to_string(), b.to_string()),
161 None => (raw.to_string(), String::new()),
162 }
163 }
164
165 fn client_for(port: u16) -> Client {
166 Client::new(
167 ClientOptions::new("slr_live_id_secret", format!("http://127.0.0.1:{port}"))
168 .backoff_ms(0),
169 )
170 .unwrap()
171 }
172
173 #[tokio::test]
180 async fn create_session_request_shape() {
181 let (l, port) = bind_mock().await;
182 let body = r#"{"sessionId":"s_1","wsEndpoint":"wss://gw.example.com/ws/s_1","cdpEndpoint":"wss://gw.example.com/cdp/s_1","expiresAt":"2026-01-01T00:00:00Z"}"#;
183 let captured = serve(l, vec![(201, body.to_string())]);
184
185 let client = client_for(port);
186 let session = client
187 .sessions()
188 .create(
189 CreateSessionOptions::new()
190 .stealth(true)
191 .proxy(ProxySpec::country("us")),
192 )
193 .await
194 .unwrap();
195
196 assert_eq!(session.id, "s_1");
197 assert_eq!(session.ws_endpoint, "wss://gw.example.com/ws/s_1");
198 assert_eq!(session.cdp_endpoint, "wss://gw.example.com/cdp/s_1");
199 assert_eq!(session.expires_at, "2026-01-01T00:00:00Z");
200 assert!(session.storage_state.is_none());
202 assert!(session.proxy.is_none());
203
204 let raw = captured.lock().unwrap()[0].clone();
205 let (head, body) = split_req(&raw);
206 assert!(head.starts_with("POST /sessions "), "head: {head}");
207 let lower = head.to_lowercase();
208 assert!(
209 lower.contains("authorization: bearer slr_live_id_secret"),
210 "head: {head}"
211 );
212 assert!(lower.contains("content-type: application/json"), "head: {head}");
213
214 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
215 assert_eq!(json.get("stealth").unwrap(), true);
216 assert_eq!(json.get("proxy").unwrap(), "us");
217 assert!(json.get("recording").is_none(), "body: {body}");
219 assert!(json.get("captcha").is_none(), "body: {body}");
220 assert!(json.get("profileId").is_none(), "body: {body}");
221 }
222
223 #[tokio::test]
225 async fn create_session_omits_empty_body() {
226 let (l, port) = bind_mock().await;
227 let body = r#"{"sessionId":"s_2","wsEndpoint":"wss://gw.example.com/ws/s_2"}"#;
228 let captured = serve(l, vec![(201, body.to_string())]);
229
230 let client = client_for(port);
231 client
232 .sessions()
233 .create(CreateSessionOptions::default())
234 .await
235 .unwrap();
236
237 let raw = captured.lock().unwrap()[0].clone();
238 let (head, body) = split_req(&raw);
239 assert!(body.trim().is_empty(), "expected no body, got: {body}");
240 assert!(
241 !head.to_lowercase().contains("content-length: 2"),
242 "head: {head}"
243 );
244 }
245
246 #[tokio::test]
248 async fn create_session_proxy_request_shape() {
249 let (l, port) = bind_mock().await;
250 let body = r#"{"sessionId":"s_3","wsEndpoint":"wss://gw.example.com/ws/s_3","proxy":{"server":"http://p:1","username":"u","password":"p","timezoneId":"America/Los_Angeles","country":"us","tier":"mobile"}}"#;
251 let captured = serve(l, vec![(201, body.to_string())]);
252
253 let client = client_for(port);
254 let session = client
255 .sessions()
256 .create(CreateSessionOptions::new().stealth(true).proxy(
257 ProxyRequest::country("us")
258 .tier(ProxyTier::Mobile)
259 .session("warm-1")
260 .session_duration(15),
261 ))
262 .await
263 .unwrap();
264
265 let proxy = session.proxy.expect("resolved proxy");
266 assert_eq!(proxy.country, "us");
267 assert_eq!(proxy.tier, Some(ProxyTier::Mobile));
268 assert_eq!(proxy.timezone_id, "America/Los_Angeles");
269
270 let raw = captured.lock().unwrap()[0].clone();
271 let (_, body) = split_req(&raw);
272 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
273 let p = json.get("proxy").unwrap();
274 assert_eq!(p.get("country").unwrap(), "us");
275 assert_eq!(p.get("tier").unwrap(), "mobile");
276 assert_eq!(p.get("session").unwrap(), "warm-1");
277 assert_eq!(p.get("sessionDuration").unwrap(), 15);
278 assert!(p.get("asn").is_none());
279 assert!(p.get("city").is_none());
280 }
281
282 #[tokio::test]
284 async fn retries_on_503() {
285 let (l, port) = bind_mock().await;
286 let ok = r#"{"sessionId":"s_ok","wsEndpoint":"wss://gw.example.com/ws/s_ok"}"#;
287 let captured = serve(
288 l,
289 vec![
290 (503, r#"{"error":"no capacity"}"#.to_string()),
291 (201, ok.to_string()),
292 ],
293 );
294
295 let client = client_for(port);
296 let session = client
297 .sessions()
298 .create(CreateSessionOptions::default())
299 .await
300 .unwrap();
301
302 assert_eq!(session.id, "s_ok");
303 assert_eq!(captured.lock().unwrap().len(), 2);
305 }
306
307 #[tokio::test]
309 async fn retries_exhausted_on_persistent_503() {
310 let (l, port) = bind_mock().await;
311 let captured = serve(
312 l,
313 vec![
314 (503, "{}".to_string()),
315 (503, "{}".to_string()),
316 (201, "{}".to_string()), ],
318 );
319
320 let client = client_for(port);
321 let err = client
322 .sessions()
323 .create(CreateSessionOptions::default())
324 .await
325 .unwrap_err();
326
327 assert!(matches!(err, SolariError::Transport { .. }), "{err:?}");
328 assert!(err.to_string().contains("exhausted 2 attempts"), "{err}");
329 assert_eq!(captured.lock().unwrap().len(), 2);
330 }
331
332 #[tokio::test]
334 async fn does_not_retry_on_400() {
335 let (l, port) = bind_mock().await;
336 let captured = serve(
337 l,
338 vec![
339 (400, r#"{"error":"bad proxy country"}"#.to_string()),
340 (201, "{}".to_string()), ],
342 );
343
344 let client = client_for(port);
345 let err = client
346 .sessions()
347 .create(CreateSessionOptions::default())
348 .await
349 .unwrap_err();
350
351 assert_eq!(err.status(), Some(400));
352 assert!(err.to_string().contains("bad proxy country"), "{err}");
353 assert_eq!(captured.lock().unwrap().len(), 1);
355 }
356
357 #[tokio::test]
360 async fn parses_error_codes() {
361 let (l, port) = bind_mock().await;
362 serve(
363 l,
364 vec![
365 (
366 402,
367 r#"{"code":"FeatureRequiresPlan","error":"captcha requires a paid plan"}"#
368 .to_string(),
369 ),
370 (
371 429,
372 r#"{"code":"ConcurrencyLimitExceeded"}"#.to_string(),
373 ),
374 (400, r#"{"code":"SomethingNew"}"#.to_string()),
375 (400, "not json at all".to_string()),
376 ],
377 );
378
379 let client = client_for(port);
380
381 let err = client
382 .sessions()
383 .create(CreateSessionOptions::new().captcha(true))
384 .await
385 .unwrap_err();
386 assert_eq!(err.status(), Some(402));
387 assert_eq!(err.code(), Some(&SolariErrorCode::FeatureRequiresPlan));
388 assert!(err.to_string().contains("captcha requires a paid plan"));
389
390 let err = client
391 .sessions()
392 .create(CreateSessionOptions::default())
393 .await
394 .unwrap_err();
395 assert_eq!(err.status(), Some(429));
396 assert_eq!(err.code(), Some(&SolariErrorCode::ConcurrencyLimitExceeded));
397
398 let err = client
400 .sessions()
401 .create(CreateSessionOptions::default())
402 .await
403 .unwrap_err();
404 assert_eq!(
405 err.code(),
406 Some(&SolariErrorCode::Other("SomethingNew".to_string()))
407 );
408
409 let err = client
411 .sessions()
412 .create(CreateSessionOptions::default())
413 .await
414 .unwrap_err();
415 assert_eq!(err.status(), Some(400));
416 assert_eq!(err.code(), None);
417 }
418
419 #[tokio::test]
421 async fn profiles_list_parses() {
422 let (l, port) = bind_mock().await;
423 let body = r#"[{"id":"p_1","name":"amazon","createdAt":"2026-01-01T00:00:00Z"},{"id":"p_2","name":"gmail"}]"#;
424 let captured = serve(l, vec![(200, body.to_string())]);
425
426 let client = client_for(port);
427 let profiles = client.profiles().list().await.unwrap();
428
429 assert_eq!(profiles.len(), 2);
430 assert_eq!(profiles[0].id, "p_1");
431 assert_eq!(profiles[0].name, "amazon");
432 assert_eq!(
434 profiles[0].extra.get("createdAt").unwrap(),
435 "2026-01-01T00:00:00Z"
436 );
437 assert_eq!(profiles[1].id, "p_2");
438 assert!(profiles[1].extra.is_empty());
439
440 let (head, _) = split_req(&captured.lock().unwrap()[0]);
441 assert!(head.starts_with("GET /profiles "), "head: {head}");
442 }
443
444 #[tokio::test]
446 async fn profiles_create_and_save() {
447 let (l, port) = bind_mock().await;
448 let captured = serve(
449 l,
450 vec![
451 (201, r#"{"id":"p_9","name":"shopify"}"#.to_string()),
452 (200, r#"{"version":3,"sizeBytes":2048}"#.to_string()),
453 (200, "{}".to_string()), ],
455 );
456
457 let client = client_for(port);
458 let profile = client.profiles().create("shopify").await.unwrap();
459 assert_eq!(profile.id, "p_9");
460 assert_eq!(profile.name, "shopify");
461
462 let state = StorageState {
463 cookies: Some(vec![Cookie {
464 name: "sid".into(),
465 value: "abc".into(),
466 domain: Some(".shopify.com".into()),
467 same_site: Some(SameSite::Lax),
468 ..Default::default()
469 }]),
470 ..Default::default()
471 };
472 let saved = client.profiles().save("p_9", &state).await.unwrap();
473 assert_eq!(saved.version, 3);
474 assert_eq!(saved.size_bytes, 2048);
475
476 let saved = client.profiles().save("p_9", &state).await.unwrap();
478 assert_eq!(saved.version, 0);
479 assert_eq!(saved.size_bytes, 0);
480
481 let reqs = captured.lock().unwrap().clone();
482 let (head, body) = split_req(&reqs[0]);
483 assert!(head.starts_with("POST /profiles "), "head: {head}");
484 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
485 assert_eq!(json.get("name").unwrap(), "shopify");
486
487 let (head, body) = split_req(&reqs[1]);
489 assert!(head.starts_with("POST /profiles/p_9/save "), "head: {head}");
490 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
491 let cookie = &json["storageState"]["cookies"][0];
492 assert_eq!(cookie.get("name").unwrap(), "sid");
493 assert_eq!(cookie.get("domain").unwrap(), ".shopify.com");
494 assert_eq!(cookie.get("sameSite").unwrap(), "Lax");
495 assert!(cookie.get("httpOnly").is_none(), "cookie: {cookie}");
497 }
498
499 #[tokio::test]
501 async fn delete_tolerates_404() {
502 let (l, port) = bind_mock().await;
503 serve(
504 l,
505 vec![
506 (404, r#"{"error":"Not Found"}"#.to_string()),
507 (404, r#"{"error":"Not Found"}"#.to_string()),
508 (500, r#"{"error":"boom"}"#.to_string()),
509 ],
510 );
511
512 let client = client_for(port);
513 client.sessions().release("s_gone").await.unwrap();
514 client.profiles().delete("p_gone").await.unwrap();
515 let err = client.sessions().release("s_bad").await.unwrap_err();
517 assert_eq!(err.status(), Some(500));
518 }
519
520 #[tokio::test]
522 async fn replay_url_defaults() {
523 let (l, port) = bind_mock().await;
524 let captured = serve(
525 l,
526 vec![
527 (200, r#"{"url":"https://s3.example.com/r1"}"#.to_string()),
528 (
529 200,
530 r#"{"url":"https://s3.example.com/r2","expiresInSeconds":600,"contentEncoding":"identity"}"#
531 .to_string(),
532 ),
533 (200, r#"{"expiresInSeconds":600}"#.to_string()),
534 ],
535 );
536
537 let client = client_for(port);
538
539 let replay = client.sessions().replay_url("s_1").await.unwrap();
540 assert_eq!(replay.url, "https://s3.example.com/r1");
541 assert_eq!(replay.expires_in_seconds, 0);
542 assert_eq!(replay.content_encoding, "gzip");
543
544 let replay = client.sessions().replay_url("s_2").await.unwrap();
545 assert_eq!(replay.expires_in_seconds, 600);
546 assert_eq!(replay.content_encoding, "identity");
547
548 let err = client.sessions().replay_url("s_3").await.unwrap_err();
550 assert!(matches!(err, SolariError::Protocol { .. }), "{err:?}");
551
552 let (head, _) = split_req(&captured.lock().unwrap()[0]);
553 assert!(head.starts_with("GET /sessions/s_1/replay-url "), "head: {head}");
554 }
555
556 #[tokio::test]
558 async fn proxy_countries() {
559 let (l, port) = bind_mock().await;
560 let captured = serve(
561 l,
562 vec![(
563 200,
564 r#"{"enabled":true,"countries":["br","de","gb","us"]}"#.to_string(),
565 )],
566 );
567
568 let client = client_for(port);
569 let res = client.proxy().countries().await.unwrap();
570 assert!(res.enabled);
571 assert_eq!(res.countries, vec!["br", "de", "gb", "us"]);
572
573 let (head, _) = split_req(&captured.lock().unwrap()[0]);
574 assert!(head.starts_with("GET /proxy/countries "), "head: {head}");
575 }
576
577 #[tokio::test]
579 async fn session_get_parses() {
580 let (l, port) = bind_mock().await;
581 serve(
582 l,
583 vec![(
584 200,
585 r#"{"sessionId":"s_1","status":"busy","expiresAt":"2026-01-01T00:00:00Z","slot":3}"#
586 .to_string(),
587 )],
588 );
589
590 let client = client_for(port);
591 let view = client.sessions().get("s_1").await.unwrap();
592 assert_eq!(view.session_id.as_deref(), Some("s_1"));
593 assert_eq!(view.status.as_deref(), Some("busy"));
594 assert_eq!(view.expires_at.as_deref(), Some("2026-01-01T00:00:00Z"));
595 assert_eq!(view.extra.get("slot").unwrap(), 3);
596 }
597
598 #[test]
600 fn cdp_derivation() {
601 assert_eq!(
602 derive_cdp_from_ws("wss://api.getsolari.com/ws/abc.def"),
603 "wss://api.getsolari.com/cdp/abc.def"
604 );
605 assert_eq!(
606 derive_cdp_from_ws("ws://127.0.0.1:3000/ws/s_1?token=x"),
607 "ws://127.0.0.1:3000/cdp/s_1?token=x"
608 );
609 assert_eq!(
611 derive_cdp_from_ws("wss://api.getsolari.com/other/abc"),
612 "wss://api.getsolari.com/other/abc"
613 );
614 assert_eq!(derive_cdp_from_ws("not a url"), "not a url");
616 }
617
618 #[tokio::test]
620 async fn create_session_derives_cdp() {
621 let (l, port) = bind_mock().await;
622 let body = r#"{"sessionId":"s_1","wsEndpoint":"wss://gw.example.com/ws/s_1"}"#;
623 serve(l, vec![(201, body.to_string())]);
624
625 let client = client_for(port);
626 let session = client
627 .sessions()
628 .create(CreateSessionOptions::default())
629 .await
630 .unwrap();
631
632 assert_eq!(session.ws_endpoint, "wss://gw.example.com/ws/s_1");
633 assert_eq!(session.cdp_endpoint, "wss://gw.example.com/cdp/s_1");
634 assert!(session.expires_at.ends_with('Z'), "{}", session.expires_at);
636 assert_eq!(session.expires_at.len(), 24, "{}", session.expires_at);
637 }
638
639 #[tokio::test]
641 async fn create_session_rejects_bad_response() {
642 let (l, port) = bind_mock().await;
643 serve(l, vec![(201, r#"{"wsEndpoint":"wss://x/ws/1"}"#.to_string())]);
644
645 let client = client_for(port);
646 let err = client
647 .sessions()
648 .create(CreateSessionOptions::default())
649 .await
650 .unwrap_err();
651 assert!(matches!(err, SolariError::Protocol { .. }), "{err:?}");
652 }
653
654 #[tokio::test]
656 async fn storage_state_fetched_from_presigned_url() {
657 let (l, port) = bind_mock().await;
658 let create = format!(
659 r#"{{"sessionId":"s_1","wsEndpoint":"wss://gw.example.com/ws/s_1","storageStateUrl":{{"url":"http://127.0.0.1:{port}/presigned","expiresInSeconds":60}}}}"#
660 );
661 let state = r#"{"cookies":[{"name":"sid","value":"abc","domain":".example.com","sameSite":"Lax"}],"origins":[{"origin":"https://example.com","localStorage":[{"name":"k","value":"v"}]}]}"#;
662 serve(l, vec![(201, create), (200, state.to_string())]);
663
664 let client = client_for(port);
665 let session = client
666 .sessions()
667 .create(CreateSessionOptions::new().profile_id("p_1"))
668 .await
669 .unwrap();
670
671 let state = session
672 .storage_state
673 .expect("profile attached → Some")
674 .expect("presigned state → Some");
675 let cookies = state.cookies.unwrap();
676 assert_eq!(cookies[0].name, "sid");
677 assert_eq!(cookies[0].same_site, Some(SameSite::Lax));
678 let origins = state.origins.unwrap();
679 assert_eq!(origins[0].origin, "https://example.com");
680 assert_eq!(origins[0].local_storage.as_ref().unwrap()[0].value, "v");
681 }
682
683 #[tokio::test]
685 async fn storage_state_absent_vs_empty() {
686 let (l, port) = bind_mock().await;
687 serve(
688 l,
689 vec![
690 (
692 201,
693 r#"{"sessionId":"s_1","wsEndpoint":"wss://x/ws/s_1","storageStateUrl":{"url":null}}"#
694 .to_string(),
695 ),
696 (
698 201,
699 r#"{"sessionId":"s_2","wsEndpoint":"wss://x/ws/s_2"}"#.to_string(),
700 ),
701 (
703 201,
704 r#"{"sessionId":"s_3","wsEndpoint":"wss://x/ws/s_3","storageState":{"cookies":[]}}"#
705 .to_string(),
706 ),
707 (
709 201,
710 r#"{"sessionId":"s_4","wsEndpoint":"wss://x/ws/s_4"}"#.to_string(),
711 ),
712 ],
713 );
714
715 let client = client_for(port);
716
717 let s = client
718 .sessions()
719 .create(CreateSessionOptions::new().profile_id("p_1"))
720 .await
721 .unwrap();
722 assert!(matches!(s.storage_state, Some(None)), "null url → Some(None)");
723
724 let s = client
725 .sessions()
726 .create(CreateSessionOptions::new().profile_id("p_1"))
727 .await
728 .unwrap();
729 assert!(matches!(s.storage_state, Some(None)), "no url → Some(None)");
730
731 let s = client
732 .sessions()
733 .create(CreateSessionOptions::new().profile_id("p_1"))
734 .await
735 .unwrap();
736 let inline = s.storage_state.expect("Some").expect("Some");
737 assert_eq!(inline.cookies.unwrap().len(), 0);
738
739 let s = client
740 .sessions()
741 .create(CreateSessionOptions::default())
742 .await
743 .unwrap();
744 assert!(s.storage_state.is_none(), "no profile → None");
745 }
746
747 #[test]
749 fn client_options_and_defaults() {
750 let opts = ClientOptions::default_region("k");
751 assert_eq!(opts.base_url, "https://api.getsolari.com");
752 assert_eq!(opts.max_attempts, 2);
753 assert_eq!(opts.backoff_ms, 500);
754 assert_eq!(opts.timeout_ms, 90_000);
755
756 assert_eq!(
757 ClientOptions::for_region("k", SolariRegion::UsWest).base_url,
758 "https://api.getsolari.com"
759 );
760
761 let c = Client::new(ClientOptions::new("k", "https://gw.example.com/")).unwrap();
763 assert_eq!(c.base_url(), "https://gw.example.com");
764
765 let err = Client::new(ClientOptions::new("", "https://gw.example.com")).unwrap_err();
767 assert!(matches!(err, SolariError::Config { .. }), "{err:?}");
768 }
769
770 #[test]
773 fn iso8601_formatting() {
774 assert_eq!(iso8601_utc(0), "1970-01-01T00:00:00.000Z");
775 assert_eq!(iso8601_utc(1_768_000_000), "2026-01-09T23:06:40.000Z");
776 assert_eq!(iso8601_utc(1_000_000_000), "2001-09-09T01:46:40.000Z");
777 assert_eq!(iso8601_utc(1_709_208_000), "2024-02-29T12:00:00.000Z");
779 assert_eq!(iso8601_utc(951_782_400), "2000-02-29T00:00:00.000Z");
781 assert_eq!(iso8601_utc(4_102_444_800), "2100-01-01T00:00:00.000Z");
783 assert_eq!(iso8601_utc(-1), "1969-12-31T23:59:59.000Z");
785 }
786
787 #[test]
789 fn error_code_round_trip() {
790 for code in [
791 SolariErrorCode::FeatureRequiresPlan,
792 SolariErrorCode::ConcurrencyLimitExceeded,
793 SolariErrorCode::PlanLimitExceeded,
794 SolariErrorCode::BrowserUnhealthy,
795 ] {
796 assert_eq!(SolariErrorCode::from(code.as_str()), code);
797 }
798 assert_eq!(
799 SolariErrorCode::from("BrowserUnhealthy"),
800 SolariErrorCode::BrowserUnhealthy
801 );
802 assert_eq!(
803 SolariErrorCode::Other("Nope".into()).to_string(),
804 "Nope".to_string()
805 );
806 }
807}