M12e: contract pass (pair invite kind, admin name in invite links, sync failed flag, rejection actor, device version name, template id, update check result, untouched accounts, D-027 re-stamp, PushEvent in :core:model, snackbar hosts)
Server: E-3 invite_kind; E-12 link ?from=<admin name>, E-4 passes it into kvitt://invite; X-Kvitt-App-Version-Name stored in devices.app_version_name (migration 0003, schema 3); OpenAPI and examples follow. Phone: :core:model, :core:fakes, :data, features and :app updated together; goldens re-recorded for S-20 (version name) and new S-1/S-22 states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -104,7 +104,7 @@ async fn health_reports_ok_with_versions() {
|
||||
assert_eq!(reply.status, StatusCode::OK);
|
||||
assert_eq!(
|
||||
reply.json,
|
||||
json!({"status": "ok", "server_version": env!("CARGO_PKG_VERSION"), "schema_version": 2, "db_ok": true})
|
||||
json!({"status": "ok", "server_version": env!("CARGO_PKG_VERSION"), "schema_version": 3, "db_ok": true})
|
||||
);
|
||||
assert_eq!(
|
||||
reply.headers["x-kvitt-server-version"],
|
||||
@@ -124,7 +124,7 @@ async fn health_is_503_degraded_when_the_database_is_gone() {
|
||||
assert_eq!(reply.status, StatusCode::SERVICE_UNAVAILABLE);
|
||||
assert_eq!(reply.json["status"], "degraded");
|
||||
assert_eq!(reply.json["db_ok"], false);
|
||||
assert_eq!(reply.json["schema_version"], 2);
|
||||
assert_eq!(reply.json["schema_version"], 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -285,6 +285,44 @@ async fn last_seen_and_app_version_are_written_at_most_hourly() {
|
||||
assert!(parse_ts(&last_seen(&app).await.0).is_some());
|
||||
}
|
||||
|
||||
async fn version_name(app: &TestApp) -> Option<String> {
|
||||
sqlx::query_scalar("SELECT app_version_name FROM devices WHERE id = ?")
|
||||
.bind(ADMIN_DEVICE_ID)
|
||||
.fetch_one(&app.state.db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn with_version_name(path: &str, name: &str) -> Request<Body> {
|
||||
let mut r = authed(Method::GET, path, &admin_token());
|
||||
r.headers_mut()
|
||||
.insert("x-kvitt-app-version-name", name.parse().unwrap());
|
||||
r
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn app_version_name_is_written_with_last_seen_and_bad_values_are_ignored() {
|
||||
let app = TestApp::household().await;
|
||||
app.send(with_version_name("/me", " 1.1 ")).await;
|
||||
assert_eq!(version_name(&app).await.as_deref(), Some("1.1"));
|
||||
|
||||
let start = app.clock.now();
|
||||
app.clock.set(start + Duration::minutes(59));
|
||||
app.send(with_version_name("/me", "1.2")).await;
|
||||
assert_eq!(version_name(&app).await.as_deref(), Some("1.1"), "hourly");
|
||||
|
||||
// An over-long or blank value keeps the stored one; no header keeps it too.
|
||||
app.clock.set(start + Duration::minutes(60));
|
||||
app.send(with_version_name("/me", &"9".repeat(33))).await;
|
||||
assert_eq!(version_name(&app).await.as_deref(), Some("1.1"));
|
||||
app.clock.set(start + Duration::minutes(120));
|
||||
app.send(authed(Method::GET, "/me", &admin_token())).await;
|
||||
assert_eq!(version_name(&app).await.as_deref(), Some("1.1"));
|
||||
app.clock.set(start + Duration::minutes(180));
|
||||
app.send(with_version_name("/me", "1.2")).await;
|
||||
assert_eq!(version_name(&app).await.as_deref(), Some("1.2"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn json_bodies_map_rejections_to_error_codes() {
|
||||
async fn echo(ApiJson(v): ApiJson<Value>) -> String {
|
||||
|
||||
Reference in New Issue
Block a user