Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

crates/gunnar-ui-web/src/sso_page.rs

//! **What the Identity provider page decides** (Administration → Identity
//! provider), on the host side of the wasm boundary: the `?account=` it was
//! opened for, and the promises about the page held to its code
//! ([`crate::code`]); the page itself is wasm-only.

/// `account` in a query string (`?a=b&account=…`), if any — decoded, so the
/// API call encodes it once (A34-50).
pub fn account_in(search: &str) -> Option<String> {
    search
        .trim_start_matches('?')
        .split('&')
        .filter_map(|p| p.split_once('='))
        .find(|(k, _)| *k == "account")
        .map(|(_, v)| query_decode(v))
        .filter(|v| !v.is_empty())
}

/// A query value's form encoding undone: `+` is a space, `%XX` a byte.
fn query_decode(value: &str) -> String {
    let b = value.as_bytes();
    let mut out = Vec::with_capacity(b.len());
    let mut i = 0;
    while i < b.len() {
        let hex = |at: usize| b.get(at).and_then(|c| (*c as char).to_digit(16));
        match b[i] {
            b'%' => match (hex(i + 1), hex(i + 2)) {
                (Some(hi), Some(lo)) => {
                    out.push((hi * 16 + lo) as u8);
                    i += 3;
                    continue;
                }
                _ => out.push(b'%'),
            },
            b'+' => out.push(b' '),
            c => out.push(c),
        }
        i += 1;
    }
    String::from_utf8_lossy(&out).into_owned()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The page's code, without these tests.
    fn page() -> &'static str {
        static CODE: std::sync::OnceLock<String> = std::sync::OnceLock::new();
        CODE.get_or_init(|| crate::code::of(include_str!("pages/identity_provider.rs")))
    }

    /// ★ **A34-50: `?account=` is decoded once**, so the API call encodes it once.
    #[test]
    fn a34_50_the_asked_account_is_decoded_before_it_is_encoded_again() {
        assert_eq!(account_in("?account=ac%20me").as_deref(), Some("ac me"));
        assert_eq!(account_in("?x=1&account=acme%2Eone").as_deref(), Some("acme.one"));
        assert_eq!(account_in("?account=a+b").as_deref(), Some("a b"));
        assert_eq!(account_in("?account=plain").as_deref(), Some("plain"));
        assert_eq!(account_in("?account="), None);
        assert_eq!(account_in(""), None);
        assert_eq!(
            crate::auth::percent_encode(&account_in("?account=ac%20me").unwrap()),
            "ac%20me",
            "encoded once, not ac%2520me"
        );
    }

    /// ★ **A34-49: the provider can be removed from the page** (`DELETE /api/account/sso`).
    #[test]
    fn a34_49_there_is_a_remove_provider_button() {
        let code = page();
        assert!(code.contains("id=\"sso-remove\""), "no Remove provider button");
        assert!(code.contains("\"Remove provider\""));
        assert!(code.contains("\"/api/account/sso?account={}\""), "the button does not call DELETE /api/account/sso");
    }

    /// ★ **A34-48: the stored client secret can be removed** (the box is gunnar's
    /// `clear_client_secret`).
    #[test]
    fn a34_48_there_is_a_remove_the_client_secret_box() {
        let code = page();
        assert!(code.contains("id=\"sso-clear-secret\""), "no remove box for the client secret");
        assert!(code.contains("\"clear_secret\": clear_secret.get_untracked()"));
    }

    /// ★ **A34-41: the IdP CA field is offered for SAML too** (gunnar reads the metadata URL
    /// trusting it).
    #[test]
    fn a34_41_the_idp_ca_field_is_offered_for_saml_too() {
        let saml = &page()[page().find("id=\"sso-saml\"").expect("the SAML block")..];
        let saml = &saml[..saml.find("</div>").unwrap()];
        assert!(saml.contains("<CaField"), "the SAML block has no IdP CA field");
    }

    /// ★ **A34-26: the page says the first passkey is trusted on the IdP sign-in alone.**
    #[test]
    fn a34_26_the_page_says_the_first_passkey_is_trusted_on_first_use() {
        assert!(
            page().contains("registers their first passkey right after their first sign-in through your identity provider"),
            "the page does not say how the first passkey is registered"
        );
    }
}