/
VVRONGG
/
backside
Обзор
Документация
Войти
/
VVRONGG
/
backside
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
master
tests/sync.rs
198 строк
7 KB
VVrongg
feat: delta-sync + routes API, hardened and tested
22 май 2026, 16:13
22 май 2026, 16:13
9d7239f
Код
Авторство
О чём код?
//! Integration tests for delta sync against a live MySQL. //! //! They exercise the cursor contract that previously broke: every create, //! update and soft-delete must surface in `?since=<cursor>` exactly once, and //! re-syncing with the returned cursor must return nothing. Assertions are made //! relative to a cursor captured at the start, so pre-existing rows in the //! shared dev database don't affect the outcome. //! //! Requires a reachable `DATABASE_URL` (loaded from `.env`); without one the //! tests skip rather than fail, so `cargo test` stays green on a bare checkout. use backside::service::{objectives, routes}; use backside::transport::dto::common::GeoPos; use backside::transport::dto::objects::{CreateObjective, UpdateObjective}; use backside::transport::dto::routes::{CreateRoute, UpdateRoute}; use sqlx::MySqlPool; use sqlx::mysql::MySqlPoolOptions; /// Connect to the test DB and apply migrations, or `None` if unavailable. async fn test_pool() -> Option<MySqlPool> { dotenvy::dotenv().ok(); let url = std::env::var("DATABASE_URL").ok()?; let pool = MySqlPoolOptions::new() .max_connections(2) .connect(&url) .await .ok()?; sqlx::migrate!("./migrations").run(&pool).await.ok()?; Some(pool) } #[tokio::test] async fn objective_sync_surfaces_each_change_exactly_once() { let Some(pool) = test_pool().await else { eprintln!("DATABASE_URL unset/unreachable — skipping objective_sync test"); return; }; // Cursor captured before we touch anything: makes the test independent of // whatever already lives in the shared dev DB. let base = objectives::sync(&pool, None).await.unwrap().server_time; let created = objectives::create( &pool, CreateObjective { name: "Sync Test Objective".into(), description: "created by integration test".into(), pos: GeoPos { lat: 1.0, lon: 2.0 }, images: vec![], }, ) .await .unwrap(); let id = created.id; // create shows up in the delta after `base` let r1 = objectives::sync(&pool, Some(base)).await.unwrap(); assert_eq!( r1.objectives.iter().filter(|o| o.id == id).count(), 1, "a new objective must appear exactly once in the delta since `base`" ); // KEY dedup property: re-syncing with the cursor we just got returns nothing let r2 = objectives::sync(&pool, Some(r1.server_time)).await.unwrap(); assert!( !r2.objectives.iter().any(|o| o.id == id), "objective must not be returned again at the cursor it was last seen at" ); // update surfaces exactly once objectives::update( &pool, id, UpdateObjective { name: None, description: Some("edited".into()), pos: None, }, ) .await .unwrap(); let r3 = objectives::sync(&pool, Some(r2.server_time)).await.unwrap(); assert_eq!( r3.objectives.iter().filter(|o| o.id == id).count(), 1, "an update must surface exactly once" ); // soft-delete surfaces once, flagged as deleted, then never again objectives::soft_delete(&pool, id).await.unwrap(); let r4 = objectives::sync(&pool, Some(r3.server_time)).await.unwrap(); let tombstones: Vec<_> = r4.objectives.iter().filter(|o| o.id == id).collect(); assert_eq!(tombstones.len(), 1, "a delete must surface exactly once"); assert!(tombstones[0].deleted, "deleted row must be flagged deleted"); let r5 = objectives::sync(&pool, Some(r4.server_time)).await.unwrap(); assert!( !r5.objectives.iter().any(|o| o.id == id), "once the tombstone is consumed the row must not reappear" ); } #[tokio::test] async fn route_sync_surfaces_each_change_exactly_once() { let Some(pool) = test_pool().await else { eprintln!("DATABASE_URL unset/unreachable — skipping route_sync test"); return; }; // a route needs a real objective to point at let obj = objectives::create( &pool, CreateObjective { name: "Route Point".into(), description: "point for route sync test".into(), pos: GeoPos { lat: 3.0, lon: 4.0 }, images: vec![], }, ) .await .unwrap(); let base = routes::sync(&pool, None).await.unwrap().server_time; let route = routes::create( &pool, CreateRoute { name: "Sync Test Route".into(), description: Some("created by integration test".into()), points: vec![obj.id], }, ) .await .unwrap(); let id = route.id; let r1 = routes::sync(&pool, Some(base)).await.unwrap(); let seen = r1.routes.iter().find(|r| r.id == id); assert!(seen.is_some(), "a new route must appear in the delta since `base`"); assert_eq!( seen.unwrap().points.as_deref(), Some([obj.id].as_slice()), "route delta must carry its ordered points" ); // dedup: re-sync at the returned cursor returns nothing let r2 = routes::sync(&pool, Some(r1.server_time)).await.unwrap(); assert!( !r2.routes.iter().any(|r| r.id == id), "route must not be returned again at its last-seen cursor" ); // touching only the points still bumps updated_at, so sync notices objectives::create( &pool, CreateObjective { name: "Second Point".into(), description: "extra point".into(), pos: GeoPos { lat: 5.0, lon: 6.0 }, images: vec![], }, ) .await .map(|o| o.id) .ok(); routes::update( &pool, id, UpdateRoute { name: Some("renamed route".into()), description: None, points: None, }, ) .await .unwrap(); let r3 = routes::sync(&pool, Some(r2.server_time)).await.unwrap(); assert_eq!( r3.routes.iter().filter(|r| r.id == id).count(), 1, "a route update must surface exactly once" ); // soft-delete surfaces once as a tombstone, then never again routes::soft_delete(&pool, id).await.unwrap(); let r4 = routes::sync(&pool, Some(r3.server_time)).await.unwrap(); let tombstones: Vec<_> = r4.routes.iter().filter(|r| r.id == id).collect(); assert_eq!(tombstones.len(), 1, "a route delete must surface exactly once"); assert!(tombstones[0].deleted, "deleted route must be flagged deleted"); let r5 = routes::sync(&pool, Some(r4.server_time)).await.unwrap(); assert!( !r5.routes.iter().any(|r| r.id == id), "once the route tombstone is consumed it must not reappear" ); }