use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use umbral::migrate::ModelMeta;
use umbral::plugin::Plugin;
use umbral_openapi::client_gen::GeneratedClient;
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "ca_post")]
pub struct CaPost {
pub id: i64,
pub title: String,
}
fn auth_paths() -> Vec<(String, Value)> {
umbral_auth::AuthPlugin::<umbral_auth::AuthUser>::default()
.with_default_routes()
.openapi_paths()
}
fn gen_client(paths: &[(String, Value)]) -> GeneratedClient {
umbral_openapi::client_gen::generate_with(
&[ModelMeta::for_::<CaPost>()],
"/api",
umbral_rest::PaginationStyle::None,
None,
&[],
paths,
)
}
#[track_caller]
fn assert_has(haystack: &str, needle: &str) {
assert!(
haystack.contains(needle),
"expected to find:\n {needle}\nin:\n{haystack}",
);
}
#[test]
fn no_auth_plugin_emits_no_session_client() {
let c = gen_client(&[]);
for absent in ["AuthClient", "readonly auth:", "LoginCredentials"] {
assert!(
!c.js.contains(absent) && !c.dts.contains(absent),
"a REST-only app must not carry a session client (`{absent}`); got:\n{}\n{}",
c.js,
c.dts,
);
}
}
#[test]
fn auth_plugin_generates_a_typed_session_client() {
let c = gen_client(&auth_paths());
assert_has(&c.dts, "export type AuthUser = {");
assert_has(&c.dts, "username: string;");
assert_has(&c.dts, "is_staff: boolean;");
assert_has(&c.dts, "id: number;");
assert!(
!c.dts.contains("username?: string;"),
"the user response declares `required`, so its fields must not be optional; got:\n{}",
c.dts,
);
assert_has(&c.dts, "export type LoginCredentials = {");
assert_has(&c.dts, "password: string;");
assert_has(&c.dts, "export interface LoginResult {");
assert_has(&c.dts, "export declare class AuthClient {");
assert_has(
&c.dts,
"login(credentials: LoginCredentials): Promise<LoginResult>;",
);
assert_has(&c.dts, "me(): Promise<AuthUser | null>;");
assert_has(&c.dts, "logout(): Promise<void>;");
assert_has(&c.dts, "readonly auth: AuthClient;");
assert_has(&c.js, "export class AuthClient {");
assert_has(&c.js, "this.auth = new AuthClient(this);");
assert_has(
&c.js,
r#"this.client._request("POST", "/api/auth/login", credentials)"#,
);
assert_has(&c.js, r#"this.client._request("GET", "/api/auth/me")"#);
}
#[test]
fn login_stores_the_token_and_requests_pick_it_up() {
let c = gen_client(&auth_paths());
assert_has(
&c.js,
"this.client._setToken(out && out.token ? out.token : null);",
);
assert_has(&c.js, "if (this._token) {");
assert_has(&c.js, "${this._token}");
assert_has(&c.js, "finally { this.client._setToken(null); }");
}
#[test]
fn me_returns_null_when_signed_out_rather_than_throwing() {
let c = gen_client(&auth_paths());
assert_has(
&c.js,
"if (err instanceof UmbralError && err.status === 401) return null;",
);
assert_has(&c.js, "throw err;");
}
#[test]
fn the_client_never_writes_the_token_to_local_storage() {
let c = gen_client(&auth_paths());
assert!(
!c.js.contains("localStorage.setItem") && !c.js.contains("sessionStorage.setItem"),
"the client must not persist the token to web storage itself; got:\n{}",
c.js,
);
assert_has(&c.js, "if (this.opts.onToken) this.opts.onToken(token);");
assert_has(&c.dts, "onToken?: (token: string | null) => void;");
}
#[test]
fn a_remounted_auth_prefix_is_honoured() {
let paths = vec![
(
"/accounts/sign-in".to_string(),
json!({ "post": {
"operationId": "auth_login",
"requestBody": {"content": {"application/json": {"schema": {
"type": "object",
"required": ["email", "password"],
"properties": {"email": {"type": "string"}, "password": {"type": "string"}}
}}}},
"responses": {"200": {"content": {"application/json": {"schema": {
"type": "object",
"required": ["user", "token"],
"properties": {
"user": {"type": "object", "required": ["id"], "properties": {"id": {"type": "integer"}}},
"token": {"type": "string"}
}
}}}}}
}}),
),
(
"/accounts/whoami".to_string(),
json!({ "get": { "operationId": "auth_me" } }),
),
];
let c = gen_client(&paths);
assert_has(
&c.js,
r#"this.client._request("POST", "/accounts/sign-in", credentials)"#,
);
assert_has(&c.js, r#"this.client._request("GET", "/accounts/whoami")"#);
assert_has(
&c.dts,
"export type LoginCredentials = { email: string; password: string; };",
);
assert!(
!c.dts.contains("register("),
"must not invent a register endpoint the app doesn't serve; got:\n{}",
c.dts,
);
}