Skip to main content

mock_nexus/
spec.rs

1//! The composable behavior description: [`Spec`] drives the mock's knobs.
2//!
3//! A [`Scenario`] is a named preset. A [`Spec`] is the same knobs laid out as
4//! independent layers a caller can compose and tweak.
5//! `Scenario -> Spec` is lossless. The drift guards stay on the enum, the
6//! runtime reads the spec.
7
8use crate::scenario::Scenario;
9
10/// The composable behavior of one hosted instance.
11#[derive(Debug, Clone, Default)]
12pub struct Spec {
13    /// Basic-auth gate: the expected `Authorization` token, base64 of `user:pass`.
14    pub auth_b64: Option<String>,
15    /// Cut the first PUT per path after this many body bytes.
16    pub partial_first_put: Option<usize>,
17    /// Cut the first success GET per path: (body bytes, lie about Content-Length).
18    pub cut_first_get: Option<(usize, bool)>,
19    /// Answer GET/HEAD bodies in delayed chunks.
20    pub drip: Option<super::server::Drip>,
21    /// Success GET/HEAD answers hide `Content-Length`.
22    pub sizeless: bool,
23    /// The first N requests per path answer 429 with a Retry-After of this many seconds.
24    pub rate_first: Option<(usize, u64)>,
25    /// The first N requests per path answer 503.
26    pub flaky_first: Option<u32>,
27    /// The first request per path answers 503 with a Retry-After of this many seconds.
28    pub pause_first_503: Option<u64>,
29    /// Refuse every ranged GET with the whole body (200).
30    pub ignore_range: bool,
31    /// Drop markers instead of storing them.
32    pub markerless: bool,
33    /// Replace marker digests with zeros.
34    pub foreign_marker: bool,
35    /// Refuse every DELETE.
36    pub read_only: bool,
37    /// Redirect reads to this path with an empty 301.
38    pub redirect: Option<String>,
39    /// Reset the first request per path.
40    pub drop_connection: bool,
41    /// Hold PUT connections without reading or answering.
42    pub freeze_upload: bool,
43}
44
45impl Spec {
46    /// The spec of a plain correct server.
47    pub fn atomic() -> Self {
48        Self::default()
49    }
50
51    /// A Basic-auth gate from plain credentials.
52    pub fn with_auth(mut self, user: &str, pass: &str) -> Self {
53        self.auth_b64 = Some(crate::base64::encode(format!("{user}:{pass}").as_bytes()));
54        self
55    }
56
57    /// Cut the first PUT per path after `bytes` body bytes.
58    pub fn with_partial_put(mut self, bytes: usize) -> Self {
59        self.partial_first_put = Some(bytes);
60        self
61    }
62
63    /// Cut the first success GET per path after `bytes` body bytes, optionally lying about `Content-Length`.
64    pub fn with_cut_body(mut self, bytes: usize, fake_length: bool) -> Self {
65        self.cut_first_get = Some((bytes, fake_length));
66        self
67    }
68
69    /// Serve GET/HEAD bodies in delayed chunks.
70    pub fn with_drip(mut self, delay_ms: u64, chunk_size: usize) -> Self {
71        self.drip = Some(super::server::Drip {
72            delay_ms,
73            chunk_size,
74        });
75        self
76    }
77
78    /// Hide `Content-Length` on success GET/HEAD answers.
79    pub fn with_sizeless(mut self) -> Self {
80        self.sizeless = true;
81        self
82    }
83
84    /// Answer the first `n` requests per path with 429 and this Retry-After.
85    pub fn with_rate_limit(mut self, n: usize, retry_after_secs: u64) -> Self {
86        self.rate_first = Some((n, retry_after_secs));
87        self
88    }
89
90    /// Answer the first `n` requests per path with 503.
91    pub fn with_flaky(mut self, n: u32) -> Self {
92        self.flaky_first = Some(n);
93        self
94    }
95
96    /// Answer the first request per path with 503 and this Retry-After.
97    pub fn with_retry_after_503(mut self, retry_after_secs: u64) -> Self {
98        self.pause_first_503 = Some(retry_after_secs);
99        self
100    }
101
102    /// Answer every ranged GET with the whole body.
103    pub fn with_ignore_range(mut self) -> Self {
104        self.ignore_range = true;
105        self
106    }
107
108    /// Drop `.sha256` PUTs instead of storing them.
109    pub fn with_markerless(mut self) -> Self {
110        self.markerless = true;
111        self
112    }
113
114    /// Zero out every stored marker's digest.
115    pub fn with_foreign_marker(mut self) -> Self {
116        self.foreign_marker = true;
117        self
118    }
119
120    /// Refuse every DELETE with 403.
121    pub fn with_read_only(mut self) -> Self {
122        self.read_only = true;
123        self
124    }
125
126    /// Redirect every read to this path with an empty 301.
127    pub fn with_redirect(mut self, location_path: &str) -> Self {
128        self.redirect = Some(location_path.to_owned());
129        self
130    }
131
132    /// Reset the first request per path right after its head.
133    pub fn with_drop_connection(mut self) -> Self {
134        self.drop_connection = true;
135        self
136    }
137
138    /// Hold PUT connections without reading or answering.
139    pub fn with_freeze_upload(mut self) -> Self {
140        self.freeze_upload = true;
141        self
142    }
143}
144
145impl From<&Scenario> for Spec {
146    fn from(scenario: &Scenario) -> Self {
147        let mut spec = Self::default();
148        match scenario {
149            Scenario::PartialPut {
150                first_attempt_bytes,
151            } => spec.partial_first_put = Some(*first_attempt_bytes),
152            Scenario::CutBody {
153                after_bytes,
154                fake_length,
155            } => spec.cut_first_get = Some((*after_bytes, *fake_length)),
156            Scenario::DropConnection => spec.drop_connection = true,
157            Scenario::FreezeUpload => spec.freeze_upload = true,
158            Scenario::Sizeless => spec.sizeless = true,
159            Scenario::Slow {
160                chunk_delay_ms,
161                chunk_size,
162            } => {
163                spec.drip = Some(super::server::Drip {
164                    delay_ms: *chunk_delay_ms,
165                    chunk_size: *chunk_size,
166                })
167            }
168            Scenario::ForeignMarker => spec.foreign_marker = true,
169            Scenario::Markerless => spec.markerless = true,
170            Scenario::Auth401 { user, pass } => {
171                spec.auth_b64 = Some(crate::base64::encode(format!("{user}:{pass}").as_bytes()))
172            }
173            Scenario::Auth403 { user, pass } => {
174                spec.auth_b64 = Some(crate::base64::encode(format!("{user}:{pass}").as_bytes()))
175            }
176            Scenario::Flaky { first_failures } => spec.flaky_first = Some(*first_failures),
177            Scenario::RetryAfter503 { retry_after_secs } => {
178                spec.pause_first_503 = Some(*retry_after_secs)
179            }
180            Scenario::RateLimit {
181                first_429s,
182                retry_after_secs,
183            } => spec.rate_first = Some((*first_429s, *retry_after_secs)),
184            Scenario::Redirect { location_path } => spec.redirect = Some(location_path.clone()),
185            Scenario::ReadOnly => spec.read_only = true,
186            Scenario::DocDrift => {}
187            Scenario::NoService => {}
188            Scenario::Search400 => {}
189            Scenario::ServiceStatusEmpty => {}
190            Scenario::ServiceStatusVersion => {}
191            Scenario::ServiceStatusDown => {}
192            Scenario::RepoCollectionTrimmed => {}
193            Scenario::RepoCollectionScoped { .. } => {}
194            Scenario::RepoDetailAdmin { .. } => {}
195            Scenario::RepoPrefixMatch => {}
196            Scenario::AssetsPagination => {}
197            Scenario::AssetsAbsent => {}
198            Scenario::AssetsPrefixQ => {}
199            Scenario::EulaGate => {}
200            Scenario::EulaAbsent => {}
201            Scenario::SearchLag { .. } => {}
202            Scenario::DetailInHint => {}
203            Scenario::IgnoreRange => spec.ignore_range = true,
204            Scenario::AuditLitter => {}
205            Scenario::Atomic => {}
206        }
207        spec
208    }
209}