import XCTest @testable import LChatCore // 🤖 Pins the native OIDC-door contract against home's Go trampoline tests // (home/oidc_test.go, home/github_test.go): the callback fragment shapes the // server emits are reproduced here verbatim, so client parsing and server // emission can't drift apart silently. final class HomeOIDCAuthTests: XCTestCase { private let base = URL(string: "https://home.example.com")! // ---- authorize URLs ----------------------------------------------------- func testGoogleAuthorizeURL() throws { let url = try XCTUnwrap(HomeOIDCAuth.authorizeURL( .google, base: base, state: "ios.abc", nonce: "n0nce")) XCTAssertEqual(url.host, "accounts.google.com") XCTAssertEqual(url.path, "/o/oauth2/v2/auth") let q = try queryDict(url) XCTAssertEqual(q["client_id"], HomeOIDCAuth.googleClientID) XCTAssertEqual(q["redirect_uri"], "https://home.example.com/oidc/return") XCTAssertEqual(q["response_type"], "id_token") XCTAssertEqual(q["response_mode"], "form_post") XCTAssertEqual(q["scope"], "openid") XCTAssertEqual(q["nonce"], "n0nce") XCTAssertEqual(q["state"], "ios.abc") } func testMicrosoftAuthorizeURL() throws { let url = try XCTUnwrap(HomeOIDCAuth.authorizeURL( .microsoft, base: base, state: "mac.xyz", nonce: "n1")) XCTAssertEqual(url.host, "login.microsoftonline.com") XCTAssertEqual(url.path, "/common/oauth2/v2.0/authorize") let q = try queryDict(url) XCTAssertEqual(q["client_id"], HomeOIDCAuth.microsoftClientID) XCTAssertEqual(q["state"], "mac.xyz") } func testGitHubAuthorizeURLNeedsClientID() throws { XCTAssertNil(HomeOIDCAuth.authorizeURL(.github, base: base, state: "s", nonce: "n")) XCTAssertNil(HomeOIDCAuth.authorizeURL(.github, base: base, state: "s", nonce: "n", githubClientID: "")) let url = try XCTUnwrap(HomeOIDCAuth.authorizeURL( .github, base: base, state: "ios.s1", nonce: "ignored", githubClientID: "Ov23liTESTID")) XCTAssertEqual(url.host, "github.com") let q = try queryDict(url) XCTAssertEqual(q["client_id"], "Ov23liTESTID") XCTAssertEqual(q["redirect_uri"], "https://home.example.com/oidc/return") XCTAssertEqual(q["state"], "ios.s1") // Code flow: no OIDC params ride along. XCTAssertNil(q["nonce"]) XCTAssertNil(q["response_type"]) } /// The web trampoline routes on this prefix — a native state must never /// carry it, or the flow lands on the web page instead of the app. func testNativeStatesNeverWebPrefixed() { for state in ["ios." + HomeAnchorAuth.mintNonce(), "mac." + HomeAnchorAuth.mintNonce()] { XCTAssertFalse(state.hasPrefix("web.")) } } // ---- callback parsing (fragments verbatim from the Go tests) ------------ func testCallbackParsesIDToken() throws { // oidc_test.go: elevenmessenger://home-oidc#state=...&id_token=tok.en.value let url = URL(string: "elevenmessenger://home-oidc#state=ios.abc&id_token=tok.en.value")! let cb = try XCTUnwrap(HomeOIDCAuth.callback(url, state: "ios.abc")) XCTAssertEqual(cb.idToken, "tok.en.value") XCTAssertNil(cb.code) } func testCallbackParsesCode() throws { // github_test.go: elevenmessenger://home-oidc#state=mac.r2&code=abc let url = URL(string: "elevenmessenger://home-oidc#state=mac.r2&code=abc")! let cb = try XCTUnwrap(HomeOIDCAuth.callback(url, state: "mac.r2")) XCTAssertEqual(cb.code, "abc") XCTAssertNil(cb.idToken) } func testCallbackRejectsWrongStateHostOrEmpty() { let ok = URL(string: "elevenmessenger://home-oidc#state=ios.a&id_token=t")! XCTAssertNil(HomeOIDCAuth.callback(ok, state: "ios.b"), "state must round-trip") let wrongHost = URL(string: "elevenmessenger://home-siwa#state=ios.a&id_token=t")! XCTAssertNil(HomeOIDCAuth.callback(wrongHost, state: "ios.a")) let bare = URL(string: "elevenmessenger://home-oidc#state=ios.a")! XCTAssertNil(HomeOIDCAuth.callback(bare, state: "ios.a"), "no credential → start over") } func testCallbackDecodesPercentEncoding() throws { // The trampoline url.QueryEscapes both fields; percent-decoding must // happen exactly once. let url = URL(string: "elevenmessenger://home-oidc#state=ios.a&id_token=a%2Fb%3Dc")! let cb = try XCTUnwrap(HomeOIDCAuth.callback(url, state: "ios.a")) XCTAssertEqual(cb.idToken, "a/b=c") } private func queryDict(_ url: URL) throws -> [String: String] { let items = try XCTUnwrap(URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems) return Dictionary(uniqueKeysWithValues: items.map { ($0.name, $0.value ?? "") }) } }