1use std::collections::BTreeMap;
4use std::path::Path;
5use std::sync::{Arc, Mutex, MutexGuard};
6
7use async_trait::async_trait;
8
9use super::cli::{ApiBranches, first_json};
10use super::{
11 Branch, Connection, CreateBranch, Function, NeonControl, NeonError, Storage, StorageCredential,
12};
13use crate::secrets::SecretValue;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
17pub enum NeonCall {
18 CreateBranch(String),
20 DeleteBranch(String),
22 ResetBranch(String),
24 CreateDatabase(String, String),
26 CreateBucket(String, String),
28 CreateCredential(String, String),
30 RevokeCredential(String, String),
32}
33
34#[derive(Clone, Default)]
35struct FakeBranch {
36 branch: Branch,
37 functions: Vec<Function>,
38 databases: Vec<String>,
39 buckets: Vec<String>,
40 credentials: BTreeMap<String, String>,
42}
43
44#[derive(Default)]
45struct Inner {
46 project: String,
47 branches: Vec<FakeBranch>,
48 calls: Vec<NeonCall>,
49 signed_in: bool,
50 next: u32,
51}
52
53#[derive(Clone)]
58pub struct FakeNeon {
59 inner: Arc<Mutex<Inner>>,
60}
61
62impl FakeNeon {
63 pub fn new(project: &str) -> Self {
66 let main = FakeBranch {
67 branch: Branch {
68 id: "br-main".into(),
69 name: "main".into(),
70 parent_id: None,
71 expires_at: None,
72 default: true,
73 annotations: BTreeMap::new(),
74 },
75 databases: vec!["neondb".into()],
76 ..FakeBranch::default()
77 };
78 FakeNeon {
79 inner: Arc::new(Mutex::new(Inner {
80 project: project.to_owned(),
81 branches: vec![main],
82 signed_in: true,
83 ..Inner::default()
84 })),
85 }
86 }
87
88 pub fn from_fixtures(project: &str, dir: &Path) -> Result<Self, String> {
92 let read = |name: &str| {
93 std::fs::read_to_string(dir.join(name)).map_err(|e| format!("{name}: {e}"))
94 };
95 let listed: Vec<Branch> = first_json(&read("branches-list.json")?)?;
96 let annotated: ApiBranches = first_json(&read("api-branches.json")?)?;
97 let annotated = annotated.into_branches();
98 let functions: Vec<Function> = first_json(&read("functions-list.json")?)?;
99 let fake = FakeNeon::new(project);
100 {
101 let mut inner = fake.lock();
102 inner.branches = listed
103 .into_iter()
104 .map(|b| {
105 let annotations = annotated
106 .iter()
107 .find(|a| a.id == b.id)
108 .map(|a| a.annotations.clone())
109 .unwrap_or_default();
110 FakeBranch {
111 functions: if b.default {
112 functions.clone()
113 } else {
114 Vec::new()
115 },
116 databases: vec!["neondb".into()],
117 branch: Branch { annotations, ..b },
118 ..FakeBranch::default()
119 }
120 })
121 .collect();
122 }
123 Ok(fake)
124 }
125
126 fn lock(&self) -> MutexGuard<'_, Inner> {
127 self.inner.lock().unwrap_or_else(|e| e.into_inner())
128 }
129
130 pub fn deploy_function(&self, name_or_id: &str, slug: &str) {
132 let mut inner = self.lock();
133 if let Some(b) = inner.branches.iter_mut().find(|b| b.branch.is(name_or_id)) {
134 let url = format!("https://{}-{slug}.compute.fake.neon.tech/", b.branch.id);
135 b.functions.retain(|f| f.slug != slug);
136 b.functions.push(Function {
137 slug: slug.to_owned(),
138 invocation_url: url,
139 });
140 }
141 }
142
143 pub fn set_signed_in(&self, signed_in: bool) {
145 self.lock().signed_in = signed_in;
146 }
147
148 pub fn calls(&self) -> Vec<NeonCall> {
150 self.lock().calls.clone()
151 }
152
153 pub fn credentials(&self, name_or_id: &str) -> Vec<String> {
155 self.lock()
156 .branches
157 .iter()
158 .find(|b| b.branch.is(name_or_id))
159 .map(|b| b.credentials.keys().cloned().collect())
160 .unwrap_or_default()
161 }
162
163 fn with_branch<T>(
164 &self,
165 project: &str,
166 name_or_id: &str,
167 f: impl FnOnce(&mut FakeBranch, &mut Vec<NeonCall>) -> T,
168 ) -> Result<T, NeonError> {
169 let mut inner = self.lock();
170 check(&inner, project)?;
171 let Inner {
172 branches, calls, ..
173 } = &mut *inner;
174 let b = branches
175 .iter_mut()
176 .find(|b| b.branch.is(name_or_id))
177 .ok_or_else(|| NeonError::NoSuchBranch(name_or_id.to_owned()))?;
178 Ok(f(b, calls))
179 }
180}
181
182fn check(inner: &Inner, project: &str) -> Result<(), NeonError> {
183 if !inner.signed_in {
184 return Err(NeonError::NotAuthenticated("signed out".into()));
185 }
186 if inner.project != project {
187 return Err(NeonError::Failed {
188 command: format!("--project-id {project}"),
189 stderr: "project not found".into(),
190 });
191 }
192 Ok(())
193}
194
195#[async_trait]
196impl NeonControl for FakeNeon {
197 async fn auth_status(&self) -> Result<String, NeonError> {
198 if self.lock().signed_in {
199 Ok("fake-user".into())
200 } else {
201 Err(NeonError::NotAuthenticated("signed out".into()))
202 }
203 }
204
205 async fn branches(&self, project: &str) -> Result<Vec<Branch>, NeonError> {
206 let inner = self.lock();
207 check(&inner, project)?;
208 Ok(inner.branches.iter().map(|b| b.branch.clone()).collect())
209 }
210
211 async fn create_branch(&self, project: &str, spec: &CreateBranch) -> Result<Branch, NeonError> {
212 let mut inner = self.lock();
213 check(&inner, project)?;
214 let parent = match &spec.parent {
215 Some(p) => inner.branches.iter().find(|b| b.branch.is(p)),
216 None => inner.branches.iter().find(|b| b.branch.default),
217 }
218 .cloned()
219 .ok_or_else(|| NeonError::NoSuchBranch(spec.parent.clone().unwrap_or_default()))?;
220 if inner.branches.iter().any(|b| b.branch.name == spec.name) {
221 return Err(NeonError::Failed {
222 command: "branches create".into(),
223 stderr: format!("branch {} already exists", spec.name),
224 });
225 }
226 inner.next += 1;
227 let id = format!("br-fake-{}", inner.next);
228 let child = FakeBranch {
231 branch: Branch {
232 id: id.clone(),
233 name: spec.name.clone(),
234 parent_id: Some(parent.branch.id.clone()),
235 expires_at: spec.expires_at.clone(),
236 default: false,
237 annotations: spec.annotations.clone(),
238 },
239 functions: parent
240 .functions
241 .iter()
242 .map(|f| Function {
243 slug: f.slug.clone(),
244 invocation_url: format!("https://{id}-{}.compute.fake.neon.tech/", f.slug),
245 })
246 .collect(),
247 databases: parent.databases.clone(),
248 buckets: parent.buckets.clone(),
249 credentials: parent.credentials.clone(),
250 };
251 let branch = child.branch.clone();
252 inner.branches.push(child);
253 inner.calls.push(NeonCall::CreateBranch(spec.name.clone()));
254 Ok(branch)
255 }
256
257 async fn delete_branch(&self, project: &str, id: &str) -> Result<(), NeonError> {
258 let mut inner = self.lock();
259 check(&inner, project)?;
260 let before = inner.branches.len();
261 inner.branches.retain(|b| b.branch.id != id);
262 if inner.branches.len() == before {
263 return Err(NeonError::NoSuchBranch(id.to_owned()));
264 }
265 inner.calls.push(NeonCall::DeleteBranch(id.to_owned()));
266 Ok(())
267 }
268
269 async fn reset_branch(&self, project: &str, id: &str) -> Result<(), NeonError> {
270 self.with_branch(project, id, |_, calls| {
271 calls.push(NeonCall::ResetBranch(id.to_owned()))
272 })
273 }
274
275 async fn connection(
276 &self,
277 project: &str,
278 branch: &str,
279 database: Option<&str>,
280 pooled: bool,
281 ) -> Result<Connection, NeonError> {
282 self.with_branch(project, branch, |b, _| {
283 let id = b.branch.id.trim_start_matches("br-");
284 let host = format!(
285 "ep-{id}{}.c-8.eu-central-1.aws.neon.tech",
286 if pooled { "-pooler" } else { "" }
287 );
288 let database = database.unwrap_or("neondb").to_owned();
289 let password = format!("fake-password-{id}");
290 let options = "sslmode=require&channel_binding=require".to_owned();
291 let userinfo = ["neondb_owner", password.as_str()].join(":");
292 Connection {
293 url: SecretValue::new(format!(
294 "postgresql://{userinfo}@{host}/{database}?{options}"
295 )),
296 host,
297 role: "neondb_owner".into(),
298 password: SecretValue::new(password),
299 database,
300 options,
301 }
302 })
303 }
304
305 async fn databases(&self, project: &str, branch: &str) -> Result<Vec<String>, NeonError> {
306 self.with_branch(project, branch, |b, _| b.databases.clone())
307 }
308
309 async fn create_database(
310 &self,
311 project: &str,
312 branch: &str,
313 name: &str,
314 ) -> Result<(), NeonError> {
315 self.with_branch(project, branch, |b, calls| {
316 b.databases.push(name.to_owned());
317 calls.push(NeonCall::CreateDatabase(
318 b.branch.id.clone(),
319 name.to_owned(),
320 ));
321 })
322 }
323
324 async fn functions(&self, project: &str, branch: &str) -> Result<Vec<Function>, NeonError> {
325 self.with_branch(project, branch, |b, _| b.functions.clone())
326 }
327
328 async fn storage(&self, project: &str, branch: &str) -> Result<Storage, NeonError> {
329 self.with_branch(project, branch, |b, _| Storage {
330 enabled: true,
331 s3_endpoint: format!(
332 "https://{}.storage.c-8.eu-central-1.aws.neon.tech",
333 b.branch.id
334 ),
335 region: "eu-central-1".into(),
336 force_path_style: true,
337 })
338 }
339
340 async fn buckets(&self, project: &str, branch: &str) -> Result<Vec<String>, NeonError> {
341 self.with_branch(project, branch, |b, _| b.buckets.clone())
342 }
343
344 async fn create_bucket(
345 &self,
346 project: &str,
347 branch: &str,
348 name: &str,
349 ) -> Result<(), NeonError> {
350 self.with_branch(project, branch, |b, calls| {
351 b.buckets.push(name.to_owned());
352 calls.push(NeonCall::CreateBucket(b.branch.id.clone(), name.to_owned()));
353 })
354 }
355
356 async fn create_credential(
357 &self,
358 project: &str,
359 branch: &str,
360 name: &str,
361 ) -> Result<StorageCredential, NeonError> {
362 let n = {
363 let mut inner = self.lock();
364 inner.next += 1;
365 inner.next
366 };
367 self.with_branch(project, branch, |b, calls| {
368 let token_id = format!("nak_fake_{n:032}");
369 let secret = format!("fake-s3-secret-{name}-{n}");
370 b.credentials.insert(token_id.clone(), secret.clone());
371 calls.push(NeonCall::CreateCredential(
372 b.branch.id.clone(),
373 token_id.clone(),
374 ));
375 StorageCredential {
376 token_id,
377 secret_access_key: SecretValue::new(secret),
378 }
379 })
380 }
381
382 async fn revoke_credential(
383 &self,
384 project: &str,
385 branch: &str,
386 token_id: &str,
387 ) -> Result<(), NeonError> {
388 self.with_branch(project, branch, |b, calls| {
389 b.credentials.remove(token_id);
390 calls.push(NeonCall::RevokeCredential(
391 b.branch.id.clone(),
392 token_id.to_owned(),
393 ));
394 })
395 }
396}