WP-02: contracts and failing tests for settings, mail, jobs and scheduler
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 22:08:23 +02:00
parent 12269051c4
commit 6113af0a19
21 changed files with 1115 additions and 0 deletions

View File

@ -185,3 +185,123 @@ pub fn fixture() -> Fixture {
svc,
}
}
use domain::jobs::{JobKind, JobRun, JobStatus};
use domain::ports::{Cipher, JobRunRepository, Mailer, SettingsRepository};
use domain::settings::SmtpSettings;
#[derive(Default)]
pub struct MemSettings(pub Mutex<HashMap<String, String>>);
#[async_trait]
impl SettingsRepository for MemSettings {
async fn get(&self, key: &str) -> Result<Option<String>, DomainError> {
Ok(self.0.lock().unwrap().get(key).cloned())
}
async fn set(&self, key: &str, value: &str) -> Result<(), DomainError> {
self.0.lock().unwrap().insert(key.into(), value.into());
Ok(())
}
}
/// Reversible "encryption" so tests can assert the stored value is not plain text.
pub struct FakeCipher;
impl Cipher for FakeCipher {
fn encrypt(&self, plain: &str) -> Result<String, DomainError> {
Ok(format!("enc:{}", plain.chars().rev().collect::<String>()))
}
fn decrypt(&self, c: &str) -> Result<String, DomainError> {
c.strip_prefix("enc:")
.map(|s| s.chars().rev().collect())
.ok_or(DomainError::Storage("bad cipher text".into()))
}
}
#[derive(Default)]
pub struct MemMailer(pub Mutex<Vec<(Vec<String>, String, String)>>);
#[async_trait]
impl Mailer for MemMailer {
async fn send(
&self,
_smtp: &SmtpSettings,
to: &[String],
subject: &str,
body: &str,
) -> Result<(), DomainError> {
self.0
.lock()
.unwrap()
.push((to.to_vec(), subject.into(), body.into()));
Ok(())
}
}
#[derive(Default)]
pub struct MemJobRuns(pub Mutex<Vec<JobRun>>);
#[async_trait]
impl JobRunRepository for MemJobRuns {
async fn insert(&self, run: &JobRun) -> Result<(), DomainError> {
self.0.lock().unwrap().push(run.clone());
Ok(())
}
async fn append_log(&self, id: Uuid, line: &str) -> Result<(), DomainError> {
let mut v = self.0.lock().unwrap();
let r = v
.iter_mut()
.find(|r| r.id == id)
.ok_or(DomainError::NotFound)?;
r.log.push_str(line);
r.log.push('\n');
Ok(())
}
async fn finish(&self, id: Uuid, status: JobStatus) -> Result<(), DomainError> {
let mut v = self.0.lock().unwrap();
let r = v
.iter_mut()
.find(|r| r.id == id)
.ok_or(DomainError::NotFound)?;
r.status = status;
r.finished_at = Some(Utc::now());
Ok(())
}
async fn get(&self, id: Uuid) -> Result<Option<JobRun>, DomainError> {
Ok(self.0.lock().unwrap().iter().find(|r| r.id == id).cloned())
}
async fn list(&self, limit: u32) -> Result<Vec<JobRun>, DomainError> {
let v = self.0.lock().unwrap();
Ok(v.iter().rev().take(limit as usize).cloned().collect())
}
async fn find_running(&self, kind: JobKind) -> Result<Option<JobRun>, DomainError> {
Ok(self
.0
.lock()
.unwrap()
.iter()
.find(|r| r.kind == kind && r.status == JobStatus::Running)
.cloned())
}
async fn last_finished(&self, kind: JobKind) -> Result<Option<JobRun>, DomainError> {
Ok(self
.0
.lock()
.unwrap()
.iter()
.rev()
.find(|r| r.kind == kind && r.status != JobStatus::Running)
.cloned())
}
}
pub fn smtp() -> SmtpSettings {
SmtpSettings {
host: "mail.example.com".into(),
port: 587,
security: domain::settings::SmtpSecurity::StartTls,
username: "bot".into(),
password: "s3cret".into(),
from: "monitoring@example.com".into(),
notify_to: vec!["ops@example.com".into()],
}
}