cratefield_testing/
privacy_provider.rs1#![allow(clippy::disallowed_types)]
11#![allow(clippy::missing_panics_doc)]
14
15use async_trait::async_trait;
16use bytes::Bytes;
17use cratefield_core::{HttpClient, HttpError, StripeStyle, WebhookVerifier};
18use http::{Request, Response};
19use serde_json::{Value, json};
20use std::collections::BTreeMap;
21use std::sync::Arc;
22use std::sync::Mutex;
23use std::sync::atomic::{AtomicUsize, Ordering};
24use std::time::{SystemTime, UNIX_EPOCH};
25
26const SIGNATURE_HEADER: &str = "Cratefield-Signature";
28
29#[derive(Debug, Clone)]
31pub struct ProviderCall {
32 pub path: String,
34 pub request_id: String,
36}
37
38#[derive(Clone, Default)]
43pub struct FakePrivacyProvider {
44 inner: Arc<Inner>,
45}
46
47#[derive(Default)]
48struct Inner {
49 secret: String,
50 subjects: Mutex<BTreeMap<String, Value>>,
51 retain: Mutex<BTreeMap<String, String>>,
52 failure: Mutex<Option<(u16, String)>>,
53 calls: Mutex<Vec<ProviderCall>>,
54 applied: Mutex<BTreeMap<String, usize>>,
55 signature_failures: AtomicUsize,
56}
57
58impl FakePrivacyProvider {
59 #[must_use]
62 pub fn new(secret: impl Into<String>) -> Self {
63 Self {
64 inner: Arc::new(Inner {
65 secret: secret.into(),
66 ..Inner::default()
67 }),
68 }
69 }
70
71 #[must_use]
74 pub fn with_subject(&self, subject: &str, sections: Value) -> Self {
75 self.inner
76 .subjects
77 .lock()
78 .expect("provider lock")
79 .insert(subject.to_owned(), sections);
80 self.clone()
81 }
82
83 #[must_use]
85 pub fn retain_section(&self, section: &str, reason: &str) -> Self {
86 self.inner
87 .retain
88 .lock()
89 .expect("provider lock")
90 .insert(section.to_owned(), reason.to_owned());
91 self.clone()
92 }
93
94 pub fn set_failure(&self, status: u16, body: &str) {
96 *self.inner.failure.lock().expect("provider lock") = Some((status, body.to_owned()));
97 }
98
99 pub fn set_down(&self, down: bool) {
101 *self.inner.failure.lock().expect("provider lock") =
102 down.then(|| (503, "provider down".to_owned()));
103 }
104
105 #[must_use]
107 pub fn calls(&self) -> Vec<ProviderCall> {
108 self.inner.calls.lock().expect("provider lock").clone()
109 }
110
111 #[must_use]
114 pub fn applies(&self, request_id: &str) -> usize {
115 self.inner
116 .applied
117 .lock()
118 .expect("provider lock")
119 .get(request_id)
120 .copied()
121 .unwrap_or(0)
122 }
123
124 #[must_use]
126 pub fn holds(&self, subject: &str) -> bool {
127 self.inner
128 .subjects
129 .lock()
130 .expect("provider lock")
131 .contains_key(subject)
132 }
133
134 #[must_use]
136 pub fn signature_failures(&self) -> usize {
137 self.inner.signature_failures.load(Ordering::SeqCst)
138 }
139
140 fn sections(&self, subject: &str) -> Value {
141 self.inner
142 .subjects
143 .lock()
144 .expect("provider lock")
145 .get(subject)
146 .cloned()
147 .unwrap_or_else(|| json!([]))
148 }
149
150 fn plan(&self, subject: &str) -> Value {
151 let sections = self.sections(subject);
152 let retain = self.inner.retain.lock().expect("provider lock");
153 let planned = sections
154 .as_array()
155 .into_iter()
156 .flatten()
157 .map(|section| {
158 let name = section["name"].as_str().unwrap_or_default();
159 match retain.get(name) {
160 Some(reason) => json!({ "name": name, "action": "retain", "reason": reason }),
161 None => json!({ "name": name, "action": "delete" }),
162 }
163 })
164 .collect();
165 Value::Array(planned)
166 }
167
168 fn apply(&self, subject: &str, request_id: &str) {
170 let mut applied = self.inner.applied.lock().expect("provider lock");
171 if applied.contains_key(request_id) {
172 return;
173 }
174 self.inner
175 .subjects
176 .lock()
177 .expect("provider lock")
178 .remove(subject);
179 applied.insert(request_id.to_owned(), 1);
180 }
181}
182
183#[async_trait]
184impl HttpClient for FakePrivacyProvider {
185 async fn send(&self, request: Request<Bytes>) -> Result<Response<Bytes>, HttpError> {
186 let (parts, body) = request.into_parts();
187 if !WebhookVerifier::new(StripeStyle {
188 header: SIGNATURE_HEADER,
189 })
190 .verify(&self.inner.secret, &parts.headers, &body, unix_now())
191 {
192 self.inner.signature_failures.fetch_add(1, Ordering::SeqCst);
193 return response(401, "invalid signature");
194 }
195 if let Some((status, body)) = self.inner.failure.lock().expect("provider lock").clone() {
196 return response(status, &body);
197 }
198 let Ok(payload) = serde_json::from_slice::<Value>(&body) else {
199 return response(400, "malformed body");
200 };
201 let subject = payload["subject"].as_str().unwrap_or_default().to_owned();
202 let request_id = payload["request_id"]
203 .as_str()
204 .unwrap_or_default()
205 .to_owned();
206 let path = parts.uri.path().to_owned();
207 self.inner
208 .calls
209 .lock()
210 .expect("provider lock")
211 .push(ProviderCall {
212 path: path.clone(),
213 request_id: request_id.clone(),
214 });
215
216 if path.ends_with("/export") {
217 return json_response(&json!({ "sections": self.sections(&subject) }));
218 }
219 if path.ends_with("/erase/plan") {
220 return json_response(&json!({ "sections": self.plan(&subject) }));
221 }
222 if path.ends_with("/erase/apply") {
223 self.apply(&subject, &request_id);
224 return json_response(&json!({ "applied": true }));
225 }
226 response(404, "no such path")
227 }
228}
229
230fn unix_now() -> i64 {
231 let secs = SystemTime::now()
232 .duration_since(UNIX_EPOCH)
233 .map_or(0, |elapsed| elapsed.as_secs());
234 i64::try_from(secs).unwrap_or(i64::MAX)
235}
236
237fn response(status: u16, body: &str) -> Result<Response<Bytes>, HttpError> {
238 Response::builder()
239 .status(status)
240 .body(Bytes::from(body.to_owned()))
241 .map_err(|err| HttpError::Transport(err.to_string()))
242}
243
244fn json_response(value: &Value) -> Result<Response<Bytes>, HttpError> {
245 response(200, &value.to_string())
246}