Phase 2 backend: Google Wallet pass + profile contact form

- cardclaws-wallet::google — inline GenericObject + RS256 `savetowallet` JWT
  (Rs256Signer via jsonwebtoken; FakeGoogleSigner test double)
- POST /v1/cards/{id}/wallet/google → { saveUrl }
- POST /v1/profile/{handle}/contact — validate, rate-limit, record
  contact_form_submission, email the owner (HTML-escaped, Resend)
- WalletConfig gains google issuer id + service account email

84 backend tests; fmt + clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 12:32:21 -05:00
co-authored by Claude Opus 4.8
parent 4770efd861
commit 8ae1d8f263
20 changed files with 545 additions and 1 deletions
@@ -72,6 +72,59 @@ async fn apple_pass_endpoint_returns_valid_pkpass() {
);
}
#[tokio::test]
async fn google_pass_endpoint_returns_save_url_with_object() {
use base64::Engine;
let app = require_app!();
let token = app.register_and_token().await;
let handle = unique_handle();
let definition = json!({
"face": { "layers": [], "background": { "type": "solid", "value": "#202028" } },
"back": { "layers": [
{ "type": "contact", "fields": { "title": "Founder", "company": "RedClaw" } }
]}
});
let (_, created) = app
.request(
"POST",
"/v1/cards",
Some(&token),
Some(json!({"handle": handle, "definition": definition})),
)
.await;
let id = created["id"].as_str().unwrap();
let (status, body) = app
.request(
"POST",
&format!("/v1/cards/{id}/wallet/google"),
Some(&token),
None,
)
.await;
assert_eq!(status, StatusCode::OK);
let save_url = body["saveUrl"].as_str().unwrap();
assert!(save_url.starts_with("https://pay.google.com/gp/v/save/"));
// Decode the JWT payload and confirm it carries our object + QR barcode.
let jwt = save_url.trim_start_matches("https://pay.google.com/gp/v/save/");
let parts: Vec<&str> = jwt.split('.').collect();
assert_eq!(parts.len(), 3);
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(parts[1])
.unwrap();
let claims: serde_json::Value = serde_json::from_slice(&payload).unwrap();
assert_eq!(claims["typ"], "savetowallet");
let obj = &claims["payload"]["genericObjects"][0];
assert_eq!(
obj["barcode"]["value"],
format!("https://cardclaws.test/{handle}")
);
assert_eq!(obj["header"]["defaultValue"]["value"], "Card Owner");
}
#[tokio::test]
async fn apple_pass_requires_auth() {
let app = require_app!();