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("webBotAuth").is_none(), "body: {body}");
221 assert!(json.get("profileId").is_none(), "body: {body}");
222 }
223
224 #[tokio::test]
226 async fn create_session_omits_empty_body() {
227 let (l, port) = bind_mock().await;
228 let body = r#"{"sessionId":"s_2","wsEndpoint":"wss://gw.example.com/ws/s_2"}"#;
229 let captured = serve(l, vec![(201, body.to_string())]);
230
231 let client = client_for(port);
232 client
233 .sessions()
234 .create(CreateSessionOptions::default())
235 .await
236 .unwrap();
237
238 let raw = captured.lock().unwrap()[0].clone();
239 let (head, body) = split_req(&raw);
240 assert!(body.trim().is_empty(), "expected no body, got: {body}");
241 assert!(
242 !head.to_lowercase().contains("content-length: 2"),
243 "head: {head}"
244 );
245 }
246
247 #[tokio::test]
249 async fn create_session_proxy_request_shape() {
250 let (l, port) = bind_mock().await;
251 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"}}"#;
252 let captured = serve(l, vec![(201, body.to_string())]);
253
254 let client = client_for(port);
255 let session = client
256 .sessions()
257 .create(CreateSessionOptions::new().stealth(true).proxy(
258 ProxyRequest::country("us")
259 .tier(ProxyTier::Mobile)
260 .session("warm-1")
261 .session_duration(15),
262 ))
263 .await
264 .unwrap();
265
266 let proxy = session.proxy.expect("resolved proxy");
267 assert_eq!(proxy.country, "us");
268 assert_eq!(proxy.tier, Some(ProxyTier::Mobile));
269 assert_eq!(proxy.timezone_id, "America/Los_Angeles");
270
271 let raw = captured.lock().unwrap()[0].clone();
272 let (_, body) = split_req(&raw);
273 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
274 let p = json.get("proxy").unwrap();
275 assert_eq!(p.get("country").unwrap(), "us");
276 assert_eq!(p.get("tier").unwrap(), "mobile");
277 assert_eq!(p.get("session").unwrap(), "warm-1");
278 assert_eq!(p.get("sessionDuration").unwrap(), 15);
279 assert!(p.get("asn").is_none());
280 assert!(p.get("city").is_none());
281 }
282
283 #[tokio::test]
285 async fn retries_on_503() {
286 let (l, port) = bind_mock().await;
287 let ok = r#"{"sessionId":"s_ok","wsEndpoint":"wss://gw.example.com/ws/s_ok"}"#;
288 let captured = serve(
289 l,
290 vec![
291 (503, r#"{"error":"no capacity"}"#.to_string()),
292 (201, ok.to_string()),
293 ],
294 );
295
296 let client = client_for(port);
297 let session = client
298 .sessions()
299 .create(CreateSessionOptions::default())
300 .await
301 .unwrap();
302
303 assert_eq!(session.id, "s_ok");
304 assert_eq!(captured.lock().unwrap().len(), 2);
306 }
307
308 #[tokio::test]
310 async fn retries_exhausted_on_persistent_503() {
311 let (l, port) = bind_mock().await;
312 let captured = serve(
313 l,
314 vec![
315 (503, "{}".to_string()),
316 (503, "{}".to_string()),
317 (201, "{}".to_string()), ],
319 );
320
321 let client = client_for(port);
322 let err = client
323 .sessions()
324 .create(CreateSessionOptions::default())
325 .await
326 .unwrap_err();
327
328 assert!(matches!(err, SolariError::Transport { .. }), "{err:?}");
329 assert!(err.to_string().contains("exhausted 2 attempts"), "{err}");
330 assert_eq!(captured.lock().unwrap().len(), 2);
331 }
332
333 #[tokio::test]
335 async fn does_not_retry_on_400() {
336 let (l, port) = bind_mock().await;
337 let captured = serve(
338 l,
339 vec![
340 (400, r#"{"error":"bad proxy country"}"#.to_string()),
341 (201, "{}".to_string()), ],
343 );
344
345 let client = client_for(port);
346 let err = client
347 .sessions()
348 .create(CreateSessionOptions::default())
349 .await
350 .unwrap_err();
351
352 assert_eq!(err.status(), Some(400));
353 assert!(err.to_string().contains("bad proxy country"), "{err}");
354 assert_eq!(captured.lock().unwrap().len(), 1);
356 }
357
358 #[tokio::test]
361 async fn parses_error_codes() {
362 let (l, port) = bind_mock().await;
363 serve(
364 l,
365 vec![
366 (
367 402,
368 r#"{"code":"FeatureRequiresPlan","error":"captcha requires a paid plan"}"#
369 .to_string(),
370 ),
371 (
372 429,
373 r#"{"code":"ConcurrencyLimitExceeded"}"#.to_string(),
374 ),
375 (400, r#"{"code":"SomethingNew"}"#.to_string()),
376 (400, "not json at all".to_string()),
377 ],
378 );
379
380 let client = client_for(port);
381
382 let err = client
383 .sessions()
384 .create(CreateSessionOptions::new().captcha(true))
385 .await
386 .unwrap_err();
387 assert_eq!(err.status(), Some(402));
388 assert_eq!(err.code(), Some(&SolariErrorCode::FeatureRequiresPlan));
389 assert!(err.to_string().contains("captcha requires a paid plan"));
390
391 let err = client
392 .sessions()
393 .create(CreateSessionOptions::default())
394 .await
395 .unwrap_err();
396 assert_eq!(err.status(), Some(429));
397 assert_eq!(err.code(), Some(&SolariErrorCode::ConcurrencyLimitExceeded));
398
399 let err = client
401 .sessions()
402 .create(CreateSessionOptions::default())
403 .await
404 .unwrap_err();
405 assert_eq!(
406 err.code(),
407 Some(&SolariErrorCode::Other("SomethingNew".to_string()))
408 );
409
410 let err = client
412 .sessions()
413 .create(CreateSessionOptions::default())
414 .await
415 .unwrap_err();
416 assert_eq!(err.status(), Some(400));
417 assert_eq!(err.code(), None);
418 }
419
420 #[tokio::test]
422 async fn profiles_list_parses() {
423 let (l, port) = bind_mock().await;
424 let body = r#"[{"id":"p_1","name":"amazon","createdAt":"2026-01-01T00:00:00Z"},{"id":"p_2","name":"gmail"}]"#;
425 let captured = serve(l, vec![(200, body.to_string())]);
426
427 let client = client_for(port);
428 let profiles = client.profiles().list().await.unwrap();
429
430 assert_eq!(profiles.len(), 2);
431 assert_eq!(profiles[0].id, "p_1");
432 assert_eq!(profiles[0].name, "amazon");
433 assert_eq!(
435 profiles[0].extra.get("createdAt").unwrap(),
436 "2026-01-01T00:00:00Z"
437 );
438 assert_eq!(profiles[1].id, "p_2");
439 assert!(profiles[1].extra.is_empty());
440
441 let (head, _) = split_req(&captured.lock().unwrap()[0]);
442 assert!(head.starts_with("GET /profiles "), "head: {head}");
443 }
444
445 #[tokio::test]
447 async fn profiles_create_and_save() {
448 let (l, port) = bind_mock().await;
449 let captured = serve(
450 l,
451 vec![
452 (201, r#"{"id":"p_9","name":"shopify"}"#.to_string()),
453 (200, r#"{"version":3,"sizeBytes":2048}"#.to_string()),
454 (200, "{}".to_string()), ],
456 );
457
458 let client = client_for(port);
459 let profile = client.profiles().create("shopify").await.unwrap();
460 assert_eq!(profile.id, "p_9");
461 assert_eq!(profile.name, "shopify");
462
463 let state = StorageState {
464 cookies: Some(vec![Cookie {
465 name: "sid".into(),
466 value: "abc".into(),
467 domain: Some(".shopify.com".into()),
468 same_site: Some(SameSite::Lax),
469 ..Default::default()
470 }]),
471 ..Default::default()
472 };
473 let saved = client.profiles().save("p_9", &state).await.unwrap();
474 assert_eq!(saved.version, 3);
475 assert_eq!(saved.size_bytes, 2048);
476
477 let saved = client.profiles().save("p_9", &state).await.unwrap();
479 assert_eq!(saved.version, 0);
480 assert_eq!(saved.size_bytes, 0);
481
482 let reqs = captured.lock().unwrap().clone();
483 let (head, body) = split_req(&reqs[0]);
484 assert!(head.starts_with("POST /profiles "), "head: {head}");
485 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
486 assert_eq!(json.get("name").unwrap(), "shopify");
487
488 let (head, body) = split_req(&reqs[1]);
490 assert!(head.starts_with("POST /profiles/p_9/save "), "head: {head}");
491 let json: serde_json::Value = serde_json::from_str(body.trim()).unwrap();
492 let cookie = &json["storageState"]["cookies"][0];
493 assert_eq!(cookie.get("name").unwrap(), "sid");
494 assert_eq!(cookie.get("domain").unwrap(), ".shopify.com");
495 assert_eq!(cookie.get("sameSite").unwrap(), "Lax");
496 assert!(cookie.get("httpOnly").is_none(), "cookie: {cookie}");
498 }
499
500 #[tokio::test]
502 async fn delete_tolerates_404() {
503 let (l, port) = bind_mock().await;
504 serve(
505 l,
506 vec![
507 (404, r#"{"error":"Not Found"}"#.to_string()),
508 (404, r#"{"error":"Not Found"}"#.to_string()),
509 (500, r#"{"error":"boom"}"#.to_string()),
510 ],
511 );
512
513 let client = client_for(port);
514 client.sessions().release("s_gone").await.unwrap();
515 client.profiles().delete("p_gone").await.unwrap();
516 let err = client.sessions().release("s_bad").await.unwrap_err();
518 assert_eq!(err.status(), Some(500));
519 }
520
521 #[tokio::test]
523 async fn replay_url_defaults() {
524 let (l, port) = bind_mock().await;
525 let captured = serve(
526 l,
527 vec![
528 (200, r#"{"url":"https://s3.example.com/r1"}"#.to_string()),
529 (
530 200,
531 r#"{"url":"https://s3.example.com/r2","expiresInSeconds":600,"contentEncoding":"identity"}"#
532 .to_string(),
533 ),
534 (200, r#"{"expiresInSeconds":600}"#.to_string()),
535 ],
536 );
537
538 let client = client_for(port);
539
540 let replay = client.sessions().replay_url("s_1").await.unwrap();
541 assert_eq!(replay.url, "https://s3.example.com/r1");
542 assert_eq!(replay.expires_in_seconds, 0);
543 assert_eq!(replay.content_encoding, "gzip");
544
545 let replay = client.sessions().replay_url("s_2").await.unwrap();
546 assert_eq!(replay.expires_in_seconds, 600);
547 assert_eq!(replay.content_encoding, "identity");
548
549 let err = client.sessions().replay_url("s_3").await.unwrap_err();
551 assert!(matches!(err, SolariError::Protocol { .. }), "{err:?}");
552
553 let (head, _) = split_req(&captured.lock().unwrap()[0]);
554 assert!(head.starts_with("GET /sessions/s_1/replay-url "), "head: {head}");
555 }
556
557 #[tokio::test]
559 async fn proxy_countries() {
560 let (l, port) = bind_mock().await;
561 let captured = serve(
562 l,
563 vec![(
564 200,
565 r#"{"enabled":true,"countries":["br","de","gb","us"]}"#.to_string(),
566 )],
567 );
568
569 let client = client_for(port);
570 let res = client.proxy().countries().await.unwrap();
571 assert!(res.enabled);
572 assert_eq!(res.countries, vec!["br", "de", "gb", "us"]);
573
574 let (head, _) = split_req(&captured.lock().unwrap()[0]);
575 assert!(head.starts_with("GET /proxy/countries "), "head: {head}");
576 }
577
578 #[tokio::test]
580 async fn session_get_parses() {
581 let (l, port) = bind_mock().await;
582 serve(
583 l,
584 vec![(
585 200,
586 r#"{"sessionId":"s_1","status":"busy","expiresAt":"2026-01-01T00:00:00Z","slot":3}"#
587 .to_string(),
588 )],
589 );
590
591 let client = client_for(port);
592 let view = client.sessions().get("s_1").await.unwrap();
593 assert_eq!(view.session_id.as_deref(), Some("s_1"));
594 assert_eq!(view.status.as_deref(), Some("busy"));
595 assert_eq!(view.expires_at.as_deref(), Some("2026-01-01T00:00:00Z"));
596 assert_eq!(view.extra.get("slot").unwrap(), 3);
597 }
598
599 #[test]
601 fn cdp_derivation() {
602 assert_eq!(
603 derive_cdp_from_ws("wss://api.getsolari.com/ws/abc.def"),
604 "wss://api.getsolari.com/cdp/abc.def"
605 );
606 assert_eq!(
607 derive_cdp_from_ws("ws://127.0.0.1:3000/ws/s_1?token=x"),
608 "ws://127.0.0.1:3000/cdp/s_1?token=x"
609 );
610 assert_eq!(
612 derive_cdp_from_ws("wss://api.getsolari.com/other/abc"),
613 "wss://api.getsolari.com/other/abc"
614 );
615 assert_eq!(derive_cdp_from_ws("not a url"), "not a url");
617 }
618
619 #[tokio::test]
621 async fn create_session_derives_cdp() {
622 let (l, port) = bind_mock().await;
623 let body = r#"{"sessionId":"s_1","wsEndpoint":"wss://gw.example.com/ws/s_1"}"#;
624 serve(l, vec![(201, body.to_string())]);
625
626 let client = client_for(port);
627 let session = client
628 .sessions()
629 .create(CreateSessionOptions::default())
630 .await
631 .unwrap();
632
633 assert_eq!(session.ws_endpoint, "wss://gw.example.com/ws/s_1");
634 assert_eq!(session.cdp_endpoint, "wss://gw.example.com/cdp/s_1");
635 assert!(session.expires_at.ends_with('Z'), "{}", session.expires_at);
637 assert_eq!(session.expires_at.len(), 24, "{}", session.expires_at);
638 }
639
640 #[tokio::test]
642 async fn create_session_rejects_bad_response() {
643 let (l, port) = bind_mock().await;
644 serve(l, vec![(201, r#"{"wsEndpoint":"wss://x/ws/1"}"#.to_string())]);
645
646 let client = client_for(port);
647 let err = client
648 .sessions()
649 .create(CreateSessionOptions::default())
650 .await
651 .unwrap_err();
652 assert!(matches!(err, SolariError::Protocol { .. }), "{err:?}");
653 }
654
655 #[tokio::test]
657 async fn storage_state_fetched_from_presigned_url() {
658 let (l, port) = bind_mock().await;
659 let create = format!(
660 r#"{{"sessionId":"s_1","wsEndpoint":"wss://gw.example.com/ws/s_1","storageStateUrl":{{"url":"http://127.0.0.1:{port}/presigned","expiresInSeconds":60}}}}"#
661 );
662 let state = r#"{"cookies":[{"name":"sid","value":"abc","domain":".example.com","sameSite":"Lax"}],"origins":[{"origin":"https://example.com","localStorage":[{"name":"k","value":"v"}]}]}"#;
663 serve(l, vec![(201, create), (200, state.to_string())]);
664
665 let client = client_for(port);
666 let session = client
667 .sessions()
668 .create(CreateSessionOptions::new().profile_id("p_1"))
669 .await
670 .unwrap();
671
672 let state = session
673 .storage_state
674 .expect("profile attached → Some")
675 .expect("presigned state → Some");
676 let cookies = state.cookies.unwrap();
677 assert_eq!(cookies[0].name, "sid");
678 assert_eq!(cookies[0].same_site, Some(SameSite::Lax));
679 let origins = state.origins.unwrap();
680 assert_eq!(origins[0].origin, "https://example.com");
681 assert_eq!(origins[0].local_storage.as_ref().unwrap()[0].value, "v");
682 }
683
684 #[tokio::test]
686 async fn storage_state_absent_vs_empty() {
687 let (l, port) = bind_mock().await;
688 serve(
689 l,
690 vec![
691 (
693 201,
694 r#"{"sessionId":"s_1","wsEndpoint":"wss://x/ws/s_1","storageStateUrl":{"url":null}}"#
695 .to_string(),
696 ),
697 (
699 201,
700 r#"{"sessionId":"s_2","wsEndpoint":"wss://x/ws/s_2"}"#.to_string(),
701 ),
702 (
704 201,
705 r#"{"sessionId":"s_3","wsEndpoint":"wss://x/ws/s_3","storageState":{"cookies":[]}}"#
706 .to_string(),
707 ),
708 (
710 201,
711 r#"{"sessionId":"s_4","wsEndpoint":"wss://x/ws/s_4"}"#.to_string(),
712 ),
713 ],
714 );
715
716 let client = client_for(port);
717
718 let s = client
719 .sessions()
720 .create(CreateSessionOptions::new().profile_id("p_1"))
721 .await
722 .unwrap();
723 assert!(matches!(s.storage_state, Some(None)), "null url → Some(None)");
724
725 let s = client
726 .sessions()
727 .create(CreateSessionOptions::new().profile_id("p_1"))
728 .await
729 .unwrap();
730 assert!(matches!(s.storage_state, Some(None)), "no url → Some(None)");
731
732 let s = client
733 .sessions()
734 .create(CreateSessionOptions::new().profile_id("p_1"))
735 .await
736 .unwrap();
737 let inline = s.storage_state.expect("Some").expect("Some");
738 assert_eq!(inline.cookies.unwrap().len(), 0);
739
740 let s = client
741 .sessions()
742 .create(CreateSessionOptions::default())
743 .await
744 .unwrap();
745 assert!(s.storage_state.is_none(), "no profile → None");
746 }
747
748 #[test]
750 fn client_options_and_defaults() {
751 let opts = ClientOptions::default_region("k");
752 assert_eq!(opts.base_url, "https://api.getsolari.com");
753 assert_eq!(opts.max_attempts, 2);
754 assert_eq!(opts.backoff_ms, 500);
755 assert_eq!(opts.timeout_ms, 90_000);
756
757 assert_eq!(
758 ClientOptions::for_region("k", SolariRegion::UsWest).base_url,
759 "https://api.getsolari.com"
760 );
761
762 let c = Client::new(ClientOptions::new("k", "https://gw.example.com/")).unwrap();
764 assert_eq!(c.base_url(), "https://gw.example.com");
765
766 let err = Client::new(ClientOptions::new("", "https://gw.example.com")).unwrap_err();
768 assert!(matches!(err, SolariError::Config { .. }), "{err:?}");
769 }
770
771 #[test]
774 fn iso8601_formatting() {
775 assert_eq!(iso8601_utc(0), "1970-01-01T00:00:00.000Z");
776 assert_eq!(iso8601_utc(1_768_000_000), "2026-01-09T23:06:40.000Z");
777 assert_eq!(iso8601_utc(1_000_000_000), "2001-09-09T01:46:40.000Z");
778 assert_eq!(iso8601_utc(1_709_208_000), "2024-02-29T12:00:00.000Z");
780 assert_eq!(iso8601_utc(951_782_400), "2000-02-29T00:00:00.000Z");
782 assert_eq!(iso8601_utc(4_102_444_800), "2100-01-01T00:00:00.000Z");
784 assert_eq!(iso8601_utc(-1), "1969-12-31T23:59:59.000Z");
786 }
787
788 #[test]
790 fn error_code_round_trip() {
791 for code in [
792 SolariErrorCode::FeatureRequiresPlan,
793 SolariErrorCode::ConcurrencyLimitExceeded,
794 SolariErrorCode::PlanLimitExceeded,
795 SolariErrorCode::BrowserUnhealthy,
796 ] {
797 assert_eq!(SolariErrorCode::from(code.as_str()), code);
798 }
799 assert_eq!(
800 SolariErrorCode::from("BrowserUnhealthy"),
801 SolariErrorCode::BrowserUnhealthy
802 );
803 assert_eq!(
804 SolariErrorCode::Other("Nope".into()).to_string(),
805 "Nope".to_string()
806 );
807 }
808}