WP-02: encrypted settings, SMTP mail, job runner and cron scheduler
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / ui (push) Has been cancelled

AES-256-GCM secret storage keyed by MASTER_KEY, SMTP settings with test mail
(lettre), persisted job runs with log and status, JobRunner with per-kind
concurrency guard, 6-field cron schedules with defaults, scheduler loop.
Settings and Jobs pages in the UI. FAKE_HOST mode for dev machines.

Tests: 30 application, 8 infrastructure, 23 API, 16 Vitest, 6 Playwright.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-02 22:16:56 +02:00
parent 6113af0a19
commit fa9ac9a6bc
31 changed files with 1632 additions and 58 deletions

View File

@ -1,11 +1,22 @@
//! Helpers to build a fully wired app on an in-memory database for integration tests.
use crate::{build_app, AppState, Config};
use std::sync::{Arc, Mutex};
use application::{JobHandler, JobLog, JobRunner};
use async_trait::async_trait;
use axum::Router;
use domain::jobs::JobKind;
use domain::ports::Mailer;
use domain::settings::SmtpSettings;
use domain::DomainError;
use crate::{build_app, AppState, Config};
pub fn test_config() -> Config {
Config {
database_url: "sqlite::memory:".into(),
jwt_secret: "test-secret-test-secret-test-secret-1234".into(),
master_key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f".into(),
fake_host: true,
bind: "127.0.0.1:0".parse().unwrap(),
bootstrap_admin: None,
cookie_secure: false,
@ -13,6 +24,45 @@ pub fn test_config() -> Config {
}
}
/// Mails "sent" by all test apps of this process.
static SENT: Mutex<Vec<(Vec<String>, String, String)>> = Mutex::new(Vec::new());
pub struct RecordingMailer;
#[async_trait]
impl Mailer for RecordingMailer {
async fn send(
&self,
_: &SmtpSettings,
to: &[String],
subject: &str,
body: &str,
) -> Result<(), DomainError> {
SENT.lock()
.unwrap()
.push((to.to_vec(), subject.into(), body.into()));
Ok(())
}
}
pub fn sent_mails(_app: &Router) -> Vec<(Vec<String>, String, String)> {
SENT.lock().unwrap().clone()
}
pub struct TestJob;
#[async_trait]
impl JobHandler for TestJob {
async fn run(&self, _params: Option<String>, log: &dyn JobLog) -> Result<(), String> {
log.line("test handler ran").await;
Ok(())
}
}
pub fn register_test_jobs(runner: JobRunner) -> JobRunner {
runner.register(JobKind::PackageRefresh, Arc::new(TestJob))
}
pub async fn build_test_app() -> Router {
build_test_app_with(test_config()).await
}
@ -29,7 +79,8 @@ async fn build_test_app_with(cfg: Config) -> Router {
let pool = infrastructure::connect(&cfg.database_url)
.await
.expect("db");
let state = AppState::new(cfg, pool);
let state = AppState::with_adapters(cfg, pool, Arc::new(RecordingMailer), register_test_jobs)
.expect("state");
state.bootstrap().await.expect("bootstrap");
build_app(state)
}