use axum::body::Body;
use axum::http::{Request, StatusCode};
use axum::Router;
use laterite_admin::settings::{save, BrandSetting};
use laterite_admin::{router, AdminConfig, Contributions};
use laterite_auth::{AuthConfig, AuthService, NewOperator, RequestContext};
use laterite_core::{CatalogStore, Db};
use std::sync::Arc;
use tower::ServiceExt;
async fn test_db() -> (Db, laterite_core::testing::TestGuard) {
laterite_core::testing::connect_test(&laterite_admin::builtin_migrations()).await
}
fn app(db: Db, app_name: &str) -> Router {
app_at(db, app_name, "/admin")
}
fn app_at(db: Db, app_name: &str, path: &str) -> Router {
let auth = AuthService::new(db.clone(), AuthConfig::default());
let mut config = AdminConfig::default();
config.app_name = app_name.to_string();
config.path = path.to_string();
router(
auth,
db,
Contributions::default(),
config,
Arc::new(CatalogStore::default()),
)
}
async fn get(router: Router, path: &str) -> (StatusCode, String) {
let resp = router
.oneshot(
Request::builder()
.method("GET")
.uri(path)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let status = resp.status();
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
(status, String::from_utf8(bytes.to_vec()).unwrap())
}
#[tokio::test]
async fn configured_app_name_is_the_brand() {
let (db, _guard) = test_db().await;
let (status, html) = get(app(db, "Acme Blog"), "/admin/setup").await;
assert_eq!(status, StatusCode::OK);
assert!(html.contains("Acme Blog"), "shows the configured app name");
assert!(!html.contains("<h1>Laterite</h1>"), "no hardcoded brand");
}
#[tokio::test]
async fn branding_form_prefills_the_configured_name() {
let (db, _guard) = test_db().await;
let auth = AuthService::new(db.clone(), AuthConfig::default());
auth.create_superuser(NewOperator {
username: "root",
email: "root@acme.test",
first_name: "Root",
last_name: None,
password: "rootpw12345",
timezone: None,
})
.await
.unwrap();
let token = auth
.authenticate("root", "rootpw12345", &RequestContext::default())
.await
.unwrap()
.token;
let resp = app(db, "Configured Name")
.oneshot(
Request::builder()
.method("GET")
.uri("/admin/settings/branding")
.header("cookie", format!("laterite_session={token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let html = String::from_utf8(bytes.to_vec()).unwrap();
assert!(
html.contains(r#"value="Configured Name""#),
"branding form seeds the configured app name into the field"
);
}
#[tokio::test]
async fn powered_by_laterite_attribution_is_shown() {
let (db, _guard) = test_db().await;
let (status, html) = get(app(db.clone(), "Acme"), "/admin/setup").await;
assert_eq!(status, StatusCode::OK);
assert!(
html.contains("Powered by Laterite"),
"setup shows attribution"
);
assert!(
html.contains("https://laterite.rs"),
"attribution links to the site"
);
let auth = AuthService::new(db.clone(), AuthConfig::default());
auth.create_superuser(NewOperator {
username: "root",
email: "root@acme.test",
first_name: "Root",
last_name: None,
password: "rootpw12345",
timezone: None,
})
.await
.unwrap();
let token = auth
.authenticate("root", "rootpw12345", &RequestContext::default())
.await
.unwrap()
.token;
let resp = app(db, "Acme")
.oneshot(
Request::builder()
.method("GET")
.uri("/admin")
.header("cookie", format!("laterite_session={token}"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let bytes = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let html = String::from_utf8(bytes.to_vec()).unwrap();
assert!(
html.contains("Powered by Laterite"),
"the in-app user menu shows the attribution"
);
}
#[tokio::test]
async fn custom_admin_path_moves_the_whole_panel() {
let (db, _guard) = test_db().await;
let (status, html) = get(app_at(db.clone(), "Acme", "/manage"), "/manage/setup").await;
assert_eq!(status, StatusCode::OK);
assert!(
html.contains("Acme"),
"the panel renders under the new mount"
);
assert!(
html.contains(r#"action="/manage/setup""#),
"the form posts under the mount"
);
assert!(
html.contains("/manage/assets/laterite.") && html.contains(".css"),
"assets load under the mount"
);
assert!(
!html.contains("/admin/"),
"nothing points at the default mount"
);
let (status, _) = get(app_at(db, "Acme", "/manage"), "/admin/setup").await;
assert_eq!(status, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn brand_setting_overrides_the_configured_name() {
let (db, _guard) = test_db().await;
save(
&db,
&BrandSetting {
app_name: "Override Co".to_string(),
},
)
.await
.unwrap();
let (status, html) = get(app(db, "Acme Blog"), "/admin/setup").await;
assert_eq!(status, StatusCode::OK);
assert!(html.contains("Override Co"), "the setting overrides config");
assert!(!html.contains("Acme Blog"), "the config name is overridden");
}