#![allow(unreachable_pub)]
#![allow(dead_code)]
#![allow(clippy::disallowed_types)]
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use async_trait::async_trait;
use axum::body::Body;
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, header};
use http::header::AUTHORIZATION;
use serde_json::Value;
use time::{Duration, OffsetDateTime};
use cratefield_core::{Auth, AuthError, Caller, Clock, Config, MapConfig, Module, Ports, Subject};
use cratefield_module_orgs::Orgs;
use cratefield_testing::TestHarness;
pub const ADMIN: &str = "test-admin-token-0123456789abcdef";
const BASE_EPOCH: i64 = 1_800_000_000;
pub const ORG_NAME: &str = "Acme";
#[derive(Clone)]
pub struct TestClock(Arc<AtomicI64>);
impl Default for TestClock {
fn default() -> Self {
Self::new()
}
}
impl TestClock {
#[must_use]
pub fn new() -> Self {
Self(Arc::new(AtomicI64::new(BASE_EPOCH)))
}
pub fn advance_secs(&self, secs: i64) {
self.0.fetch_add(secs, Ordering::SeqCst);
}
pub fn reset(&self) {
self.0.store(BASE_EPOCH, Ordering::SeqCst);
}
#[must_use]
pub fn read(&self) -> OffsetDateTime {
OffsetDateTime::from_unix_timestamp(self.0.load(Ordering::SeqCst)).expect("in range")
}
}
impl Clock for TestClock {
fn now(&self) -> OffsetDateTime {
self.read()
}
}
#[derive(Clone, Default)]
pub struct TestAuth;
impl TestAuth {
#[must_use]
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Auth for TestAuth {
async fn identify(&self, headers: &HeaderMap) -> Result<Caller, AuthError> {
let Some(value) = headers.get(AUTHORIZATION) else {
return Ok(Caller::Anonymous);
};
let token = value
.to_str()
.map_err(|_| AuthError::NotVerified)?
.strip_prefix("Bearer ")
.ok_or(AuthError::NotVerified)?
.trim();
match token.split_once('|') {
Some((sub, email)) => Ok(Caller::Subject(
Subject::new(sub).email(Some(email.to_owned())),
)),
None => Ok(Caller::Subject(Subject::new(token))),
}
}
}
#[derive(Clone)]
pub struct Spec {
pub roles: Vec<String>,
pub managers: Vec<String>,
pub staff_org: String,
pub staff_roles: Vec<String>,
pub invitation_ttl_secs: i64,
}
impl Default for Spec {
fn default() -> Self {
Self {
roles: vec!["owner".to_owned(), "manager".to_owned(), "staff".to_owned()],
managers: vec!["manager".to_owned()],
staff_org: String::new(),
staff_roles: Vec::new(),
invitation_ttl_secs: 7 * 86_400,
}
}
}
impl Spec {
#[must_use]
pub fn with_staff(org_id: &str, role: &str) -> Self {
let mut roles = Self::default().roles;
roles.push(role.to_owned());
Self {
roles,
staff_org: org_id.to_owned(),
staff_roles: vec![role.to_owned()],
..Self::default()
}
}
}
pub struct Kit {
pub harness: TestHarness,
pub clock: TestClock,
}
impl Kit {
pub async fn get(&self, path: &str, as_: &str) -> Res {
send(self, Method::GET, path, Some(as_), None).await
}
pub async fn post(&self, path: &str, as_: &str, json: &str) -> Res {
send(self, Method::POST, path, Some(as_), Some(json)).await
}
pub async fn patch(&self, path: &str, as_: &str, json: &str) -> Res {
send(self, Method::PATCH, path, Some(as_), Some(json)).await
}
pub async fn delete(&self, path: &str, as_: &str) -> Res {
send(self, Method::DELETE, path, Some(as_), None).await
}
pub async fn create_org(&self, owner: &str, name: &str) -> String {
let response = self
.post("/v1/orgs", owner, &format!(r#"{{"name":"{name}"}}"#))
.await;
assert_eq!(
response.status,
StatusCode::CREATED,
"{:?}",
response.text()
);
response.json()["id"]
.as_str()
.expect("the created org carries an id")
.to_owned()
}
pub async fn add_member(&self, org_id: &str, actor: &str, sub: &str, role: &str) -> Res {
self.post(
&format!("/v1/orgs/{org_id}/members"),
actor,
&format!(r#"{{"sub":"{sub}","role":"{role}"}}"#),
)
.await
}
pub async fn seed_org(&self, id: &str, name: &str, sub: &str, role: &str) {
const SEEDED_AT: &str = "2026-01-02T00:00:00Z";
self.harness
.db
.batch_atomic(&[
cratefield_core::Statement::with_values(
"INSERT INTO orgs (id, name, created_by, created_at) VALUES (?, ?, ?, ?)",
vec![id.into(), name.into(), sub.into(), SEEDED_AT.into()],
),
cratefield_core::Statement::with_values(
"INSERT INTO org_members (org_id, user_sub, role, invited_by, created_at) \
VALUES (?, ?, ?, NULL, ?)",
vec![id.into(), sub.into(), role.into(), SEEDED_AT.into()],
),
])
.await
.expect("seeds the organization");
}
pub async fn seed_member(&self, org_id: &str, sub: &str, role: &str) {
self.harness
.db
.execute(&cratefield_core::Statement::with_values(
"INSERT INTO org_members (org_id, user_sub, role, invited_by, created_at) \
VALUES (?, ?, ?, NULL, ?)",
vec![
org_id.into(),
sub.into(),
role.into(),
"2026-01-02T00:00:00Z".into(),
],
))
.await
.expect("seeds the membership");
}
pub async fn rows(&self, table: &str) -> usize {
self.harness
.db
.query(&cratefield_core::Statement::new(format!(
"SELECT * FROM {table}"
)))
.await
.expect("counts the rows")
.len()
}
pub async fn rows_for(&self, table: &str, column: &str, subject: &str) -> i64 {
let rows = self
.harness
.db
.query(&cratefield_core::Statement::with_values(
format!("SELECT COUNT(*) AS n FROM {table} WHERE {column} = ?"),
vec![subject.into()],
))
.await
.expect("counts the subject rows");
rows.first().and_then(|row| row.get("n")).unwrap_or(0)
}
pub async fn owners_in(&self, org_id: &str) -> i64 {
self.harness
.db
.query(&cratefield_core::Statement::with_values(
"SELECT COUNT(*) AS n FROM org_members WHERE org_id = ? AND role = 'owner'",
vec![org_id.into()],
))
.await
.expect("counts the owners")
.first()
.and_then(|row| row.get("n"))
.unwrap_or(0)
}
#[must_use]
pub fn invitation_token(&self) -> String {
let message = self
.harness
.mailer
.last_message()
.expect("an invitation mail was sent");
let marker = "?token=";
let start = message
.text
.find(marker)
.expect("the accept link carries the token")
+ marker.len();
let rest = &message.text[start..];
let end = rest
.find(|ch: char| ch.is_whitespace())
.unwrap_or(rest.len());
percent_decode(&rest[..end])
}
}
pub struct Fixture {
pub kits: Vec<Kit>,
pub clock: TestClock,
}
#[must_use]
pub fn fixture(spec: &Spec) -> Fixture {
fixture_with(spec, Vec::new)
}
#[must_use]
pub fn fixture_with(spec: &Spec, extra: impl Fn() -> Vec<Box<dyn Module>>) -> Fixture {
let config: Arc<dyn Config> = Arc::new(MapConfig::from_pairs([(
String::from("ADMIN_TOKEN"),
ADMIN.to_owned(),
)]));
build(spec, extra, &config)
}
#[must_use]
pub fn fixture_without_admin(spec: &Spec) -> Fixture {
let config: Arc<dyn Config> = Arc::new(MapConfig::default());
build(spec, Vec::new, &config)
}
#[must_use]
fn build(
spec: &Spec,
extra: impl Fn() -> Vec<Box<dyn Module>>,
config: &Arc<dyn Config>,
) -> Fixture {
let clock = TestClock::new();
let spec_for_modules = spec.clone();
let make = move || {
let module = Orgs::builder()
.roles(spec_for_modules.roles.clone())
.managers(spec_for_modules.managers.clone())
.staff_org(spec_for_modules.staff_org.clone())
.staff_roles(spec_for_modules.staff_roles.clone())
.invitation_ttl(Duration::seconds(spec_for_modules.invitation_ttl_secs))
.build();
let mut modules = vec![Box::new(module) as Box<dyn Module>];
modules.extend(extra());
modules
};
let clock_for_ports = clock.clone();
let config_for_ports = Arc::clone(config);
let harnesses = TestHarness::all_dialects_with_ports(make, move |ports: &mut Ports| {
ports.clock = Some(Arc::new(clock_for_ports.clone()));
ports.auth = Some(Arc::new(TestAuth::new()));
ports.config = Arc::clone(&config_for_ports);
});
let kits = harnesses
.into_iter()
.map(|harness| Kit {
harness,
clock: clock.clone(),
})
.collect();
Fixture { kits, clock }
}
#[must_use]
pub fn module() -> Orgs {
Orgs::builder()
.roles(["owner", "manager", "staff"])
.managers(["manager"])
.build()
}
pub struct Res {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Vec<u8>,
}
impl Res {
#[must_use]
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
#[must_use]
pub fn json(&self) -> Value {
serde_json::from_slice(&self.body)
.unwrap_or_else(|err| panic!("body is not JSON ({err}): {}", self.text()))
}
#[must_use]
pub fn problem_slug(&self) -> String {
self.json()["type"].as_str().map_or_else(
|| panic!("no problem type: {}", self.text()),
|uri| uri.rsplit('/').next().unwrap_or(uri).to_owned(),
)
}
}
pub async fn send(
kit: &Kit,
method: Method,
path: &str,
as_: Option<&str>,
json: Option<&str>,
) -> Res {
call(
kit.harness.router.clone(),
build_request(method, path, as_, json),
)
.await
}
pub fn race_send(
router: axum::Router,
method: Method,
path: &str,
as_: &str,
json: Option<&str>,
) -> Res {
pollster::block_on(call(router, build_request(method, path, Some(as_), json)))
}
fn build_request(
method: Method,
path: &str,
as_: Option<&str>,
json: Option<&str>,
) -> Request<Body> {
let mut builder = Request::builder().method(method).uri(path);
if let Some(token) = as_ {
builder = builder.header(
header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}")).expect("a header value"),
);
}
let body = match json {
Some(payload) => {
builder = builder.header(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
Body::from(payload.to_owned())
}
None => Body::empty(),
};
builder.body(body).expect("request builds")
}
async fn call(router: axum::Router, request: Request<Body>) -> Res {
use tower::ServiceExt;
let response = router.oneshot(request).await.expect("router answers");
let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, 1024 * 1024)
.await
.expect("body reads");
Res {
status: parts.status,
headers: parts.headers,
body: bytes.to_vec(),
}
}
#[must_use]
pub fn percent_decode(value: &str) -> String {
let bytes = value.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or("");
if let Ok(byte) = u8::from_str_radix(hex, 16) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
}