maarcadetweet: initial commit

AT Protocol PDS + AppView + Tauri Desktop Client, 160-char post limit.

- PDS (Rust + axum + sqlx)
  - Auth: createAccount, createSession, refreshSession
  - Records: createRecord, deleteRecord (race-safe via SELECT FOR UPDATE)
  - Feed: feed.like.create, feed.repost.create
  - Sync: getRepo, getBlocks, getLatestCommit, getRecord (with MST proof), listRepos
  - Identity: resolveHandle
  - MST: spec-conformant (at-mst crate, 27 tests)
  - Repo: signed commits, TID counter (monotonic, 4096 wrap safe)

- AppView (Rust + axum + sqlx)
  - Jetstream consumer (WebSocket, exponential backoff, 38k+ events indexed)
  - REST API: timeline/home (graph-aware), profile, search, post (with thread hydration)
  - Handle-sync worker (did:plc + did:web)
  - JSONB embed storage + thread columns (migration 0003)
  - Like/repost counter cache (migration 0004)

- Tauri 2 + Svelte 5 Desktop Client
  - System tray (Show/Compose/Quit menu)
  - OS notifications (tauri-plugin-notification)
  - Auto-update (tauri-plugin-updater, placeholder endpoint)
  - Window-state (tauri-plugin-window-state)
  - 160-char compose with live counter
  - Image/Link embed rendering
  - LocalStorage-persisted like state
  - Timeline with poll (prepend new posts)
  - Custom TitleBar (transparent, no decorations)
  - Orange/IBM Plex Mono maarcade design

Tests: 231 Rust + 9 vitest = 240 passed.
This commit is contained in:
tomdebone
2026-07-05 20:01:31 +02:00
commit c586fd39c9
134 changed files with 35279 additions and 0 deletions
@@ -0,0 +1,142 @@
//! Integration tests for [`WebResolver`].
//!
//! These spin up a tiny mock HTTP server on `127.0.0.1:0` (just like
//! the PLC tests do). Because the DID-to-URL builder splits on `:`
//! to turn path components into URL segments, encoding `127.0.0.1:PORT`
//! in a DID doesn't produce a URL the mock can answer — so the tests
//! drive the resolver through its crate-internal `resolve_handle_at_url`
//! seam, which takes a URL directly. Production code never calls it;
//! tests do, so we can exercise the full HTTP round-trip without TLS.
//!
//! Each test runs the resolver inside a 2-second timeout so a hung
//! connection can't freeze the suite.
use at_identity::WebResolver;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;
/// Spawn a single-shot mock HTTP server that always responds with
/// `status` + `body`, regardless of path. Returns the base URL.
async fn mock_server(status: u16, body: &'static [u8]) -> String {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
let (mut sock, _) = match listener.accept().await {
Ok(p) => p,
Err(_) => return,
};
tokio::spawn(async move {
let mut buf = vec![0u8; 4096];
let _ = sock.read(&mut buf).await;
let reason = match status {
200 => "OK",
404 => "Not Found",
500 => "Internal Server Error",
_ => "Status",
};
let header = format!(
"HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
let _ = sock.write_all(header.as_bytes()).await;
if !body.is_empty() {
let _ = sock.write_all(body).await;
}
let _ = sock.shutdown().await;
});
}
});
format!("http://{addr}")
}
fn resolver() -> WebResolver {
WebResolver {
client: Client::builder()
.timeout(Duration::from_secs(2))
.build()
.unwrap(),
scheme: "https".to_string(),
}
}
async fn resolve(r: &WebResolver, base: &str, path: &str) -> anyhow::Result<Option<String>> {
let url = format!("{base}{path}");
tokio::time::timeout(
Duration::from_secs(2),
r.resolve_handle_at_url(&url),
)
.await
.expect("timed out talking to mock server")
}
/// Happy path: a DID document with
/// `alsoKnownAs: ["at://alice.example.com"]` must yield
/// `Some("alice.example.com")`.
#[tokio::test]
async fn resolve_web_handle_returns_handle_from_alsoKnownAs() {
let body = br#"{"id":"did:web:example.com","alsoKnownAs":["at://alice.example.com"],"verificationMethod":[]}"#;
let base = mock_server(200, body).await;
let r = resolver();
let got = resolve(&r, &base, "/.well-known/did.json").await.unwrap();
assert_eq!(got.as_deref(), Some("alice.example.com"));
}
/// A 404 from the remote must collapse to `Ok(None)`, never `Err`.
#[tokio::test]
async fn resolve_web_handle_handles_404() {
let base = mock_server(404, b"").await;
let r = resolver();
let got = resolve(&r, &base, "/.well-known/did.json").await.unwrap();
assert!(got.is_none(), "404 must yield None, got {got:?}");
}
/// A 2xx with a non-JSON body (think: an HTML error page served by
/// a misconfigured reverse proxy) must also collapse to `Ok(None)`
/// so the worker doesn't see it as a transient failure.
#[tokio::test]
async fn resolve_web_handle_handles_invalid_json() {
let base = mock_server(200, b"<html>not json</html>").await;
let r = resolver();
let got = resolve(&r, &base, "/.well-known/did.json").await.unwrap();
assert!(got.is_none(), "invalid JSON must yield None, got {got:?}");
}
/// A 2xx with valid JSON but no `alsoKnownAs` field must yield
/// `Ok(None)` (the document doesn't advertise a handle).
#[tokio::test]
async fn resolve_web_handle_handles_missing_alsoKnownAs() {
let body = br#"{"id":"did:web:example.com","verificationMethod":[]}"#;
let base = mock_server(200, body).await;
let r = resolver();
let got = resolve(&r, &base, "/.well-known/did.json").await.unwrap();
assert!(got.is_none(), "missing alsoKnownAs must yield None");
}
/// A `alsoKnownAs` array that doesn't contain any `at://` URI must
/// yield `Ok(None)`.
#[tokio::test]
async fn resolve_web_handle_ignores_non_at_uris() {
let body = br#"{"id":"did:web:example.com","alsoKnownAs":["https://example.com","mailto:foo"]}"#;
let base = mock_server(200, body).await;
let r = resolver();
let got = resolve(&r, &base, "/.well-known/did.json").await.unwrap();
assert!(
got.is_none(),
"non-at:// entries must not be treated as handles, got {got:?}"
);
}
/// Multiple `alsoKnownAs` entries: the **first** `at://` wins. This
/// matches the PLC client's behavior and the way real-world PDSes
/// list their primary handle first.
#[tokio::test]
async fn resolve_web_handle_picks_first_at_uri() {
let body = br#"{"id":"did:web:example.com","alsoKnownAs":["at://first.example.com","at://second.example.com"]}"#;
let base = mock_server(200, body).await;
let r = resolver();
let got = resolve(&r, &base, "/.well-known/did.json").await.unwrap();
assert_eq!(got.as_deref(), Some("first.example.com"));
}