mod router_construction {
use std::sync::Arc;
use sqlx::postgres::PgPoolOptions;
use crate::api::scim::{ScimState, scim_router};
#[tokio::test]
async fn scim_router_constructs() {
let pool = PgPoolOptions::new()
.connect_lazy("postgres://scim:scim@127.0.0.1:1/scim")
.expect("lazy pool");
let state = ScimState {
tokens: Arc::new(fraiseql_auth::scim::PgScimTokenStore::new(pool.clone())),
session_store: Arc::new(fraiseql_auth::PostgresSessionStore::new(pool.clone())),
rbac: Arc::new(crate::api::rbac_management::db_backend::RbacDbBackend::new(
pool.clone(),
)),
pool,
base_url: "https://api.example.com/scim/v2".to_string(),
};
let _router = scim_router(state);
}
}
mod filter {
#![allow(clippy::unwrap_used)]
use crate::api::scim::filter::{expect_attribute, parse_eq};
#[test]
fn parses_the_shape_provisioning_clients_actually_send() {
let parsed = parse_eq(r#"userName eq "alice@example.com""#).expect("should parse");
assert_eq!(parsed.attribute, "username");
assert_eq!(parsed.value, "alice@example.com");
assert_eq!(expect_attribute(&parsed, "userName").unwrap(), "alice@example.com");
}
#[test]
fn operator_and_attribute_are_case_insensitive() {
let parsed = parse_eq(r#"USERNAME EQ "bob""#).expect("RFC 7644 folds both");
assert_eq!(parsed.attribute, "username");
assert_eq!(expect_attribute(&parsed, "userName").unwrap(), "bob");
}
#[test]
fn escaped_quotes_survive() {
let parsed = parse_eq(r#"userName eq "a\"b""#).expect("should parse");
assert_eq!(parsed.value, "a\"b");
}
#[test]
fn an_unsupported_filter_is_refused_never_ignored() {
assert!(parse_eq(r#"userName sw "ali""#).is_err(), "only eq is supported");
assert!(parse_eq("userName eq alice").is_err(), "value must be quoted");
assert!(parse_eq("userName").is_err(), "a bare attribute is not a filter");
assert!(
parse_eq(r#"userName eq "a" and active eq true"#).is_err(),
"composition must be refused, not silently truncated to the first term"
);
assert!(
parse_eq(r#"userName eq "a" or userName eq "b""#).is_err(),
"disjunction must be refused"
);
let parsed = parse_eq(r#"externalId eq "x""#).expect("parses");
assert!(expect_attribute(&parsed, "userName").is_err());
}
}
mod concurrency {
#![allow(clippy::unwrap_used)]
use axum::http::{HeaderMap, HeaderValue, header};
use crate::api::scim::{precondition_failed, resources::etag};
fn if_match(value: &str) -> HeaderMap {
let mut headers = HeaderMap::new();
headers.insert(header::IF_MATCH, HeaderValue::from_str(value).unwrap());
headers
}
#[test]
fn a_matching_version_passes_and_a_stale_one_is_refused() {
assert!(precondition_failed(&if_match(&etag(3)), 3).is_none(), "current version passes");
assert!(precondition_failed(&if_match("*"), 3).is_none(), "* always passes");
assert!(precondition_failed(&HeaderMap::new(), 3).is_none(), "absent If-Match passes");
assert!(precondition_failed(&if_match(&etag(2)), 3).is_some(), "stale version refused");
}
#[test]
fn the_weak_marker_is_optional_and_a_candidate_list_is_accepted() {
assert!(precondition_failed(&if_match("\"3\""), 3).is_none(), "bare entity tag matches");
assert!(
precondition_failed(&if_match(&format!("{}, {}", etag(2), etag(3))), 3).is_none(),
"any candidate in the list may match"
);
}
}
mod search_body {
#![allow(clippy::panic)]
use serde_json::json;
use crate::api::scim::{ListQuery, SearchBody, resources::project};
const TESTER_BODY: &str = r#"{"attributes":["externalId"],"schemas":["urn:ietf:params:scim:api:messages:2.0:SearchRequest"]}"#;
fn parse(body: &str) -> ListQuery {
serde_json::from_str::<SearchBody>(body)
.unwrap_or_else(|e| panic!("SearchBody must accept {body}: {e}"))
.into()
}
#[test]
fn the_array_form_of_attributes_is_accepted() {
let q = parse(TESTER_BODY);
assert_eq!(q.attributes.as_deref(), Some("externalId"));
}
#[test]
fn a_multi_valued_attributes_array_keeps_every_name() {
let q = parse(r#"{"attributes":["userName","displayName"]}"#);
assert_eq!(q.attributes.as_deref(), Some("userName,displayName"));
let projected = project(
json!({"id": "u1", "userName": "ada", "displayName": "Ada", "externalId": "x"}),
q.attributes.as_deref(),
q.excluded.as_deref(),
);
assert!(projected.get("userName").is_some(), "requested attribute survives");
assert!(projected.get("displayName").is_some(), "second requested attribute survives");
assert!(projected.get("externalId").is_none(), "unrequested attribute is dropped");
}
#[test]
fn the_array_form_of_excluded_attributes_is_accepted() {
let q = parse(r#"{"excludedAttributes":["displayName","externalId"]}"#);
assert_eq!(q.excluded.as_deref(), Some("displayName,externalId"));
}
#[test]
fn the_comma_separated_string_form_still_parses() {
let q = parse(r#"{"attributes":"userName,displayName"}"#);
assert_eq!(q.attributes.as_deref(), Some("userName,displayName"));
}
#[test]
fn an_absent_or_null_attributes_field_stays_absent() {
assert_eq!(parse(r#"{"filter":"userName eq \"ada\""}"#).attributes, None);
assert_eq!(parse(r#"{"attributes":null}"#).attributes, None);
assert_eq!(parse(r#"{"attributes":[]}"#).attributes.as_deref(), Some(""));
}
}