Phase 1 foundation: backend, web profile, and mobile app
Greenfield implementation of CardClaws Phase 1 across three surfaces. Backend (Rust/Axum workspace, 67 tests): - cardclaws-types/config/db/auth/api/wallet crates - Auth: register, login + lockout, magic link, refresh rotation, Apple verify - Cards: CRUD, tier-limited publish, duplicate, public handle lookup - Assets: R2 presigned uploads; vCard export - Apple Wallet .pkpass pipeline (PKCS#7 signer behind apple-signing feature) - Analytics ingest + summary with daily-salted IP hashing - Migrations 0001 (incl. cardclaws_sessions) + 0002 analytics Web profile (Astro SSR): cardclaws.com/[handle] hero + flip, contact actions, client-built vCard, visit attribution. Verified end-to-end. Mobile (Expo SDK 51): auth, card list/create, builder v1 (bg/text/logo, palette, undo/redo), Skia/Reanimated viewer (flip + ambient). 22 logic tests. CI: policy/backend/profile/mobile jobs; LOC + no-placeholder lint. Review fixes baked in: `back` (not `cardclaws`) key; strip image is a bundled manifest file (not a URL); sessions table added; NFC reframed; test doubles allowed for external services. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
//! Integration test for the Apple Wallet pass endpoint (PRD §13.3). Exercises
|
||||
//! the full path: card -> service -> wallet crate -> `.pkpass` bytes.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
use common::unique_handle;
|
||||
|
||||
#[tokio::test]
|
||||
async fn apple_pass_endpoint_returns_valid_pkpass() {
|
||||
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": {
|
||||
"email": "[email protected]",
|
||||
"company": "RedClaw",
|
||||
"title": "Founder"
|
||||
}}
|
||||
]}
|
||||
});
|
||||
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, content_type, bytes) = app
|
||||
.request_bytes(
|
||||
"POST",
|
||||
&format!("/v1/cards/{id}/wallet/apple"),
|
||||
Some(&token),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(status, StatusCode::OK);
|
||||
assert_eq!(content_type, "application/vnd.apple.pkpass");
|
||||
// A .pkpass is a zip — magic bytes "PK".
|
||||
assert_eq!(&bytes[0..2], b"PK");
|
||||
|
||||
// Open the bundle and confirm pass.json's QR points at this card's profile.
|
||||
let mut archive = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
let names: Vec<String> = (0..archive.len())
|
||||
.map(|i| archive.by_index(i).unwrap().name().to_string())
|
||||
.collect();
|
||||
assert!(names.iter().any(|n| n == "pass.json"));
|
||||
assert!(names.iter().any(|n| n == "strip.png"));
|
||||
assert!(names.iter().any(|n| n == "manifest.json"));
|
||||
assert!(names.iter().any(|n| n == "signature"));
|
||||
|
||||
let mut pass_json = String::new();
|
||||
archive
|
||||
.by_name("pass.json")
|
||||
.unwrap()
|
||||
.read_to_string(&mut pass_json)
|
||||
.unwrap();
|
||||
let pass: serde_json::Value = serde_json::from_str(&pass_json).unwrap();
|
||||
assert_eq!(
|
||||
pass["barcode"]["message"],
|
||||
format!("https://cardclaws.test/{handle}")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn apple_pass_requires_auth() {
|
||||
let app = require_app!();
|
||||
let (status, _, _) = app
|
||||
.request_bytes(
|
||||
"POST",
|
||||
"/v1/cards/00000000-0000-0000-0000-000000000000/wallet/apple",
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
Reference in New Issue
Block a user