mod common;
use common::run_lua;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn api_key_header() -> &'static str {
"API-Key test-name$test-secret"
}
fn api_key_lua_literal() -> &'static str {
"test-name$test-secret"
}
#[tokio::test]
async fn test_health_ok() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/health"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
assert.eq(c.sys:health(), true)
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_health_false_on_5xx() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/health"))
.respond_with(ResponseTemplate::new(503))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
assert.eq(c.sys:health(), false)
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_discovery_config() {
let server = MockServer::start().await;
let issuer = server.uri();
Mock::given(method("GET"))
.and(path("/.well-known/openid-configuration"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"issuer": issuer,
"authorization_endpoint": format!("{issuer}/oidc/authorize"),
"token_endpoint": format!("{issuer}/oidc/token"),
"jwks_uri": format!("{issuer}/oidc/certs"),
"id_token_signing_alg_values_supported": ["EdDSA", "RS256"]
})))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local cfg = c.discovery:config()
assert.eq(cfg.issuer, "{}")
assert.eq(cfg.token_endpoint, "{}/oidc/token")
"#,
server.uri(),
api_key_lua_literal(),
server.uri(),
server.uri()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_clients_get_existing() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/openbao"))
.and(header("Authorization", api_key_header()))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "openbao",
"name": "OpenBao",
"confidential": true,
"challenges": ["S256"]
})))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local got = c.clients:get("openbao")
assert.eq(got.id, "openbao")
assert.eq(got.confidential, true)
assert.eq(got.challenges[1], "S256")
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_clients_get_missing_returns_nil() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/missing"))
.respond_with(ResponseTemplate::new(404))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
assert.eq(c.clients:get("missing"), nil)
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_rotate_secret() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/clients/openbao/secret"))
.and(header("Authorization", api_key_header()))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"secret": "freshly-rotated-secret-value"
})))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local s = c.clients:rotate_secret("openbao")
assert.eq(s, "freshly-rotated-secret-value")
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_reconcile_noop_when_no_drift() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "openbao",
"name": "OpenBao",
"confidential": true,
"challenges": ["S256"],
"redirect_uris": ["https://o.example.com/cb"]
})))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local r = c.clients:reconcile({{
id = "openbao", name = "OpenBao",
confidential = true, challenges = {{ "S256" }},
redirect_uris = {{ "https://o.example.com/cb" }},
}})
assert.eq(r.action, "noop")
assert.eq(r.secret, nil)
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_reconcile_put_when_payload_omits_field_present_on_server() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "openbao",
"name": "OpenBao",
"confidential": true,
"challenges": ["S256"],
"redirect_uris": ["https://o.example.com/cb"]
})))
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local r = c.clients:reconcile({{
id = "openbao", name = "OpenBao",
confidential = true,
redirect_uris = {{ "https://o.example.com/cb" }},
}})
assert.eq(r.action, "put")
assert.eq(r.drift_on, "challenges")
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_reconcile_create_on_404() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(404))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/clients"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/clients/openbao/secret"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"secret": "minted-on-create"
})))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local r = c.clients:reconcile({{
id = "openbao", name = "OpenBao",
confidential = true, challenges = {{ "S256" }},
redirect_uris = {{ "https://o.example.com/cb" }},
}})
assert.eq(r.action, "create")
assert.eq(r.secret, "minted-on-create")
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_reconcile_rebuild_on_challenges_drift() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "openbao",
"name": "OpenBao",
"confidential": true,
"challenges": [],
"redirect_uris": ["https://o.example.com/cb"]
})))
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/clients"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/clients/openbao/secret"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"secret": "rotated-after-rebuild"
})))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local r = c.clients:reconcile({{
id = "openbao", name = "OpenBao",
confidential = true, challenges = {{ "S256" }},
redirect_uris = {{ "https://o.example.com/cb" }},
}})
assert.eq(r.action, "rebuild")
assert.eq(r.reason, "challenges-drift")
assert.eq(r.secret, "rotated-after-rebuild")
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_reconcile_put_only_on_non_challenges_drift() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"id": "openbao",
"name": "OpenBao",
"confidential": true,
"challenges": ["S256"],
"redirect_uris": ["https://o.example.com/cb"],
"id_token_alg": "EdDSA"
})))
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/clients/openbao"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let script = format!(
r#"
local rauthy = require("assay.rauthy")
local c = rauthy.client("{}", "{}")
local r = c.clients:reconcile({{
id = "openbao", name = "OpenBao",
confidential = true, challenges = {{ "S256" }},
redirect_uris = {{ "https://o.example.com/cb" }},
id_token_alg = "RS256",
}})
assert.eq(r.action, "put")
assert.eq(r.drift_on, "id_token_alg")
assert.eq(r.secret, nil)
"#,
server.uri(),
api_key_lua_literal()
);
run_lua(&script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_openbao() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.openbao({ host = "openbao.example.com" })
assert.eq(p.id, "openbao")
assert.eq(p.confidential, true)
assert.eq(p.id_token_alg, "RS256")
assert.eq(p.access_token_alg, "RS256")
assert.eq(p.challenges[1], "S256")
assert.eq(p.redirect_uris[1], "https://openbao.example.com/ui/vault/auth/oidc/oidc/callback")
assert.eq(p.redirect_uris[2], "http://localhost:8250/oidc/callback")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_argocd() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.argocd({ host = "argocd.example.com" })
assert.eq(p.id, "argocd")
assert.eq(p.confidential, false)
assert.eq(p.id_token_alg, "EdDSA")
assert.eq(p.challenges[1], "S256")
assert.eq(p.redirect_uris[1], "https://argocd.example.com/auth/callback")
assert.eq(p.redirect_uris[2], "http://localhost:8085/auth/callback")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_outline() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.outline({ host = "wiki.example.com" })
assert.eq(p.id, "outline")
assert.eq(p.name, "Outline")
assert.eq(p.confidential, true)
assert.eq(p.id_token_alg, "RS256")
assert.eq(p.access_token_alg, "RS256")
-- Outline 1.8.x sends authorize without code_challenge; the preset
-- intentionally omits `challenges` so Rauthy doesn't enforce PKCE.
assert.eq(p.challenges, nil)
assert.eq(p.redirect_uris[1], "https://wiki.example.com/auth/oidc.callback")
assert.eq(p.scopes[1], "openid")
assert.eq(p.scopes[2], "email")
assert.eq(p.scopes[3], "profile")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_outline_requires_host() {
let script = r#"
local rauthy = require("assay.rauthy")
local ok, err = pcall(rauthy.client_presets.outline, {})
assert.eq(ok, false)
assert.eq(string.find(err, "opts.host required") ~= nil, true)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_openbao_requires_host() {
let script = r#"
local rauthy = require("assay.rauthy")
local ok, err = pcall(rauthy.client_presets.openbao, {})
assert.eq(ok, false)
assert.eq(string.find(err, "opts.host required") ~= nil, true)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_seafile() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.seafile({ host = "seafile.example.com" })
assert.eq(p.id, "seafile")
assert.eq(p.name, "Seafile")
assert.eq(p.confidential, true)
assert.eq(p.id_token_alg, "RS256")
assert.eq(p.challenges, nil)
assert.eq(p.redirect_uris[1], "https://seafile.example.com/oauth/callback/")
assert.eq(p.scopes[4], "groups")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_seafile_requires_host() {
let script = r#"
local rauthy = require("assay.rauthy")
local ok, err = pcall(rauthy.client_presets.seafile, {})
assert.eq(ok, false)
assert.eq(string.find(err, "opts.host required") ~= nil, true)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_paperless() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.paperless({ host = "paperless.example.com" })
assert.eq(p.id, "paperless")
assert.eq(p.name, "Paperless-ngx")
assert.eq(p.confidential, true)
assert.eq(p.id_token_alg, "RS256")
-- Paperless docs explicitly recommend PKCE
-- (OAUTH_PKCE_ENABLED: true), so Rauthy must accept S256.
assert.eq(p.challenges[1], "S256")
assert.eq(p.redirect_uris[1],
"https://paperless.example.com/accounts/oidc/rauthy/login/callback/")
assert.eq(p.scopes[4], "groups")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_paperless_requires_host() {
let script = r#"
local rauthy = require("assay.rauthy")
local ok, err = pcall(rauthy.client_presets.paperless, {})
assert.eq(ok, false)
assert.eq(string.find(err, "opts.host required") ~= nil, true)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_immich() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.immich({ host = "photos.example.com" })
assert.eq(p.id, "immich")
assert.eq(p.name, "Immich")
assert.eq(p.confidential, true)
assert.eq(p.id_token_alg, "RS256")
-- Immich web does NOT initiate PKCE (Authelia integration guide
-- sets require_pkce: false). Preset must omit `challenges`.
assert.eq(p.challenges, nil)
assert.eq(p.redirect_uris[1], "https://photos.example.com/auth/login")
assert.eq(p.redirect_uris[2], "https://photos.example.com/user-settings")
assert.eq(p.scopes[4], "groups")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_immich_requires_host() {
let script = r#"
local rauthy = require("assay.rauthy")
local ok, err = pcall(rauthy.client_presets.immich, {})
assert.eq(ok, false)
assert.eq(string.find(err, "opts.host required") ~= nil, true)
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_immich_mobile() {
let script = r#"
local rauthy = require("assay.rauthy")
local p = rauthy.client_presets.immich_mobile({ host = "photos.example.com" })
assert.eq(p.id, "immich-mobile")
assert.eq(p.name, "Immich (mobile)")
-- Public client (no shared secret) -> MUST use PKCE.
assert.eq(p.confidential, false)
assert.eq(p.challenges[1], "S256")
assert.eq(p.redirect_uris[1], "app.immich:///oauth-callback")
-- post_logout_redirect_uris still points at the web origin for the
-- back-channel logout fallback.
assert.eq(p.post_logout_redirect_uris[1], "https://photos.example.com")
assert.eq(p.scopes[4], "groups")
"#;
run_lua(script).await.unwrap();
}
#[tokio::test]
async fn test_client_preset_immich_mobile_requires_host() {
let script = r#"
local rauthy = require("assay.rauthy")
local ok, err = pcall(rauthy.client_presets.immich_mobile, {})
assert.eq(ok, false)
assert.eq(string.find(err, "opts.host required") ~= nil, true)
"#;
run_lua(script).await.unwrap();
}