From a480c2bfc3aa4f5516f25ccee844ed848cd0c676 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Wed, 30 Sep 2026 08:43:31 +0800 Subject: [PATCH 1/2] test(rust): cover acquisition attribution lifecycle --- apps/api-rust/src/route_contract.rs | 133 +++++- apps/api-rust/tests/acquisition.rs | 419 +++++++++++++++++- .../scripts/check-real-integration-gates.sh | 5 +- .../scripts/run-real-integration-gates.sh | 11 +- .../scripts/test-new-integration-suites.py | 4 +- 5 files changed, 559 insertions(+), 13 deletions(-) mode change 100755 => 100644 apps/api-rust/tests/scripts/check-real-integration-gates.sh mode change 100755 => 100644 apps/api-rust/tests/scripts/run-real-integration-gates.sh diff --git a/apps/api-rust/src/route_contract.rs b/apps/api-rust/src/route_contract.rs index c4f97ef1c..752b3714b 100644 --- a/apps/api-rust/src/route_contract.rs +++ b/apps/api-rust/src/route_contract.rs @@ -136,14 +136,52 @@ fn valid_contract(bytes: &[u8]) -> bool { #[cfg(test)] mod tests { use super::*; + use std::os::unix::fs::symlink; + + type TestResult = Result<(), Box>; + + struct TestDirectory(PathBuf); + + impl TestDirectory { + fn new() -> io::Result { + let path = std::env::temp_dir() + .join(format!("lmm-route-contract-test-{}", uuid::Uuid::new_v4())); + fs::create_dir(&path)?; + Ok(Self(path)) + } + + fn path(&self) -> &Path { + &self.0 + } + } + + impl Drop for TestDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } #[test] fn stable_semver_requires_one_final_newline() { - assert!(valid_contract(b"1.0.0\n")); - assert!(!valid_contract(b"01.0.0\n")); - assert!(!valid_contract(b"1.0.0")); - assert!(!valid_contract(b"1.0.0\n\n")); - assert!(!valid_contract(b"1.0.0-rc.1\n")); + for valid in [b"0.0.0\n".as_slice(), b"1.20.300\n".as_slice()] { + assert!(valid_contract(valid)); + } + for invalid in [ + b"".as_slice(), + b"1.0.0".as_slice(), + b"1.0.0\n\n".as_slice(), + b"1.0\n".as_slice(), + b"1.0.0.1\n".as_slice(), + b"1..0\n".as_slice(), + b"1.a.0\n".as_slice(), + b"01.0.0\n".as_slice(), + b"1.00.0\n".as_slice(), + b"1.0.00\n".as_slice(), + b"1.0.0-rc.1\n".as_slice(), + b"\xff.0.0\n".as_slice(), + ] { + assert!(!valid_contract(invalid)); + } } #[test] @@ -162,4 +200,89 @@ mod tests { ); Ok(()) } + + #[test] + fn generated_revision_round_trips_and_detects_contract_changes() -> TestResult { + let directory = TestDirectory::new()?; + let contract = directory.path().join("VERSION"); + let output = directory.path().join("nested/route-contract.sha256"); + fs::write(&contract, b"2.3.4\n")?; + + let digest = generate(&contract, &output)?; + assert_eq!(fs::read_to_string(&output)?, format!("{digest}\n")); + assert_eq!(verify(&contract, &output)?, digest); + assert_eq!(fs::metadata(&output)?.permissions().mode() & 0o777, 0o644); + + fs::write(&contract, b"2.3.5\n")?; + assert!(matches!( + verify(&contract, &output), + Err(RouteContractError::RevisionMismatch) + )); + let regenerated = generate(&contract, &output)?; + assert_ne!(regenerated, digest); + assert_eq!(verify(&contract, &output)?, regenerated); + + fs::write(&contract, b"2.3\n")?; + assert!(matches!( + revision(&contract), + Err(RouteContractError::InvalidVersion) + )); + Ok(()) + } + + #[test] + fn contract_and_revision_symlinks_are_rejected() -> TestResult { + let directory = TestDirectory::new()?; + let contract = directory.path().join("VERSION"); + let contract_link = directory.path().join("VERSION.link"); + let revision_file = directory.path().join("route-contract.sha256"); + let revision_link = directory.path().join("route-contract.link"); + fs::write(&contract, b"1.0.0\n")?; + generate(&contract, &revision_file)?; + symlink(&contract, &contract_link)?; + symlink(&revision_file, &revision_link)?; + + assert!(matches!( + revision(&contract_link), + Err(RouteContractError::UnsafeContract) + )); + assert!(matches!( + verify(&contract, &revision_link), + Err(RouteContractError::UnsafeRevision) + )); + assert!(matches!( + generate(&contract, &revision_link), + Err(RouteContractError::UnsafeOutput) + )); + assert!(matches!( + generate(&contract, directory.path()), + Err(RouteContractError::UnsafeOutput) + )); + assert!(matches!( + verify(&contract, &directory.path().join("missing.sha256")), + Err(RouteContractError::UnsafeRevision) + )); + Ok(()) + } + + #[test] + fn malformed_revision_is_rejected_before_comparison() -> TestResult { + let directory = TestDirectory::new()?; + let contract = directory.path().join("VERSION"); + let revision_file = directory.path().join("route-contract.sha256"); + fs::write(&contract, b"1.0.0\n")?; + + for malformed in [ + "a".repeat(64), + format!("{}\n", "A".repeat(64)), + format!("{}\n\n", "a".repeat(64)), + ] { + fs::write(&revision_file, malformed)?; + assert!(matches!( + verify(&contract, &revision_file), + Err(RouteContractError::InvalidRevision) + )); + } + Ok(()) + } } diff --git a/apps/api-rust/tests/acquisition.rs b/apps/api-rust/tests/acquisition.rs index 8c4f46e36..86f032f35 100644 --- a/apps/api-rust/tests/acquisition.rs +++ b/apps/api-rust/tests/acquisition.rs @@ -6,10 +6,11 @@ use axum::{ }; use lmm_api_rs::{ auth::{AuthConfig, PgValkeyDashboardAuth}, - routes::acquisition::{AcquisitionState, PgAcquisitionStore, router}, + routes::acquisition::{AcquisitionState, Error, Input, PgAcquisitionStore, router}, }; use secrecy::SecretString; -use sqlx::postgres::PgPoolOptions; +use serde_json::{Value, json}; +use sqlx::{PgPool, postgres::PgPoolOptions}; use tower::ServiceExt; fn app() -> axum::Router { @@ -87,3 +88,417 @@ async fn public_visit_fails_closed_without_storage_for_a_malformed_body() { Some("no-store, no-cache, must-revalidate, private, max-age=0") ); } + +struct PgFixture { + admin: PgPool, + pg: PgPool, + schema: String, + store: PgAcquisitionStore, +} + +impl PgFixture { + async fn new() -> Self { + let url = std::env::var("LMM_TEST_DATABASE_URL").expect("isolated PostgreSQL"); + let admin = PgPool::connect(&url).await.expect("admin pool"); + let schema = format!("acquisition_{}", uuid::Uuid::new_v4().simple()); + sqlx::query(&format!("CREATE SCHEMA {schema}")) + .execute(&admin) + .await + .expect("create isolated schema"); + let pg = PgPoolOptions::new() + .max_connections(8) + .after_connect({ + let schema = schema.clone(); + move |connection, _| { + let statement = format!("SET search_path TO {schema}"); + Box::pin(async move { + sqlx::query(&statement).execute(connection).await?; + Ok(()) + }) + } + }) + .connect(&url) + .await + .expect("isolated pool"); + sqlx::raw_sql( + "CREATE TABLE users(\ + id BIGINT PRIMARY KEY,\ + created_at BIGINT NOT NULL,\ + deleted_at TIMESTAMPTZ\ + );\ + CREATE TABLE casbin_rule(\ + ptype TEXT NOT NULL,\ + v0 TEXT NOT NULL,\ + v1 TEXT NOT NULL,\ + v2 TEXT NOT NULL,\ + v3 TEXT NOT NULL DEFAULT ''\ + );\ + INSERT INTO users VALUES(7,1700000000,NULL);", + ) + .execute(&pg) + .await + .expect("supporting schema"); + let migration = include_str!("../migrations/0017_acquisition_foundation.sql") + .replace("__LMM_APP_SCHEMA__", &schema); + sqlx::raw_sql(&migration) + .execute(&pg) + .await + .expect("acquisition schema"); + let store = PgAcquisitionStore::new(pg.clone()); + Self { + admin, + pg, + schema, + store, + } + } + + async fn cleanup(self) { + self.pg.close().await; + sqlx::query(&format!("DROP SCHEMA {} CASCADE", self.schema)) + .execute(&self.admin) + .await + .expect("drop isolated schema"); + self.admin.close().await; + } +} + +fn input(value: Value) -> Input { + serde_json::from_value(value).expect("acquisition input") +} + +#[tokio::test] +#[ignore = "requires isolated PostgreSQL via LMM_TEST_DATABASE_URL"] +async fn postgres_link_policy_and_lookback_contracts_are_durable() { + let fixture = PgFixture::new().await; + assert!(fixture.store.permission(7, 100, "write").await.unwrap()); + assert!(!fixture.store.permission(7, 1, "read").await.unwrap()); + sqlx::raw_sql( + "INSERT INTO casbin_rule VALUES\ + ('p','role:admin','acquisition','read','allow'),\ + ('p','user:7','acquisition','read','deny');", + ) + .execute(&fixture.pg) + .await + .unwrap(); + assert!(!fixture.store.permission(7, 10, "read").await.unwrap()); + sqlx::query("DELETE FROM casbin_rule WHERE v0='user:7'") + .execute(&fixture.pg) + .await + .unwrap(); + assert!(fixture.store.permission(7, 10, "read").await.unwrap()); + + let link = fixture + .store + .save_link(input(json!({ + "name":"Docs campaign", + "source":"community", + "medium":"documentation", + "campaign":"launch", + "content":"readme", + "target":"/guide" + }))) + .await + .unwrap(); + assert_eq!(link.id.len(), 32); + let preview = fixture.store.preview(&link.id).await.unwrap(); + assert_eq!(preview["source"], "community"); + assert_eq!(preview["target"], "/guide"); + let page = fixture.store.links(1, 20, "active", "docs").await.unwrap(); + assert_eq!(page["total"], 1); + assert_eq!(page["items"][0]["id"], link.id); + + fixture.store.lookback(30).await.unwrap(); + fixture.store.lookback(45).await.unwrap(); + fixture.store.lookback(45).await.unwrap(); + let config: i64 = + sqlx::query_scalar("SELECT lookback_days FROM acquisition_configs WHERE id=1") + .fetch_one(&fixture.pg) + .await + .unwrap(); + let policies: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM acquisition_attribution_policies") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(config, 45); + assert_eq!(policies, 2, "unchanged lookback must not duplicate history"); + + fixture.store.delete_link(&link.id).await.unwrap(); + assert!(matches!( + fixture.store.preview(&link.id).await, + Err(Error::Database(sqlx::Error::RowNotFound)) + )); + fixture.cleanup().await; +} + +#[tokio::test] +#[ignore = "requires isolated PostgreSQL via LMM_TEST_DATABASE_URL"] +async fn postgres_consent_visit_report_and_withdrawal_round_trip() { + let fixture = PgFixture::new().await; + fixture.store.grant(7).await.unwrap(); + let consent: (bool, i64) = + sqlx::query_as("SELECT allowed,version FROM acquisition_consents WHERE user_id=7") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(consent, (true, 2)); + + let link = fixture + .store + .save_link(input(json!({ + "name":"Launch", + "source":"community", + "target":"/pricing" + }))) + .await + .unwrap(); + let visitor = "a".repeat(64); + let visit_input = input(json!({ + "consent":true, + "consent_version":2, + "nonce":"0123456789abcdef0123456789abcdef", + "landing":"/pricing", + "link_id":link.id + })); + let first = fixture + .store + .observe(&visitor, 7, &visit_input, &["api.lmm.best"]) + .await + .unwrap(); + let replay = fixture + .store + .observe(&visitor, 7, &visit_input, &["api.lmm.best"]) + .await + .unwrap(); + assert_eq!(replay.id, first.id); + assert_eq!(first.source, "community"); + assert_eq!(first.evidence, "promotion_link"); + let visits: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM acquisition_visits") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(visits, 1, "visitor nonce replay must stay idempotent"); + + let report = fixture + .store + .report( + 7, + Some(input(json!({"source":"community","detail":"Forum"}))), + false, + ) + .await + .unwrap(); + assert_eq!(report["source"], "community"); + assert_eq!(report["detail"], "Forum"); + assert_eq!( + fixture.store.report(7, None, true).await.unwrap(), + Value::Null + ); + + fixture.store.withdraw(Some(&visitor), 7).await.unwrap(); + let allowed: bool = + sqlx::query_scalar("SELECT allowed FROM acquisition_consents WHERE user_id=7") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert!(!allowed); + for table in [ + "acquisition_visitors", + "acquisition_visits", + "acquisition_accounts", + ] { + let count: i64 = sqlx::query_scalar(&format!("SELECT COUNT(*) FROM {table}")) + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(count, 0, "{table}"); + } + fixture.cleanup().await; +} + +#[tokio::test] +#[ignore = "requires isolated PostgreSQL via LMM_TEST_DATABASE_URL"] +async fn postgres_registration_keeps_first_touch_and_selects_last_external_visit() { + let fixture = PgFixture::new().await; + let registered_at: i64 = + sqlx::query_scalar("SELECT EXTRACT(EPOCH FROM clock_timestamp())::BIGINT + 10") + .fetch_one(&fixture.pg) + .await + .unwrap(); + sqlx::query("UPDATE users SET created_at=$1 WHERE id=7") + .bind(registered_at) + .execute(&fixture.pg) + .await + .unwrap(); + + let visitor = "b".repeat(64); + let first = fixture + .store + .observe( + &visitor, + 0, + &input(json!({ + "consent":true, + "consent_version":1, + "nonce":"11111111111111111111111111111111", + "landing":"/guide", + "source":"documentation", + "campaign":"getting-started" + })), + &["api.lmm.best"], + ) + .await + .unwrap(); + let selected = fixture + .store + .observe( + &visitor, + 0, + &input(json!({ + "consent":true, + "consent_version":1, + "nonce":"22222222222222222222222222222222", + "landing":"/pricing", + "source":"community", + "campaign":"launch" + })), + &["api.lmm.best"], + ) + .await + .unwrap(); + let latest = fixture + .store + .observe( + &visitor, + 0, + &input(json!({ + "consent":true, + "consent_version":2, + "nonce":"33333333333333333333333333333333", + "landing":"/sign-up" + })), + &["api.lmm.best"], + ) + .await + .unwrap(); + + fixture + .store + .attribute_registration(7, &visitor) + .await + .unwrap(); + let account: Value = sqlx::query_scalar( + "SELECT to_jsonb(acquisition_accounts) FROM acquisition_accounts WHERE user_id=7", + ) + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(account["first_visit_id"], first.id); + assert_eq!(account["first_source"], "documentation"); + assert_eq!(account["first_evidence"], "campaign_parameters"); + assert_eq!(account["registration_visit_id"], selected.id); + assert_eq!(account["registration_source"], "community"); + assert_eq!(account["registration_campaign"], "launch"); + assert_eq!(account["registration_evidence"], "campaign_parameters"); + assert_eq!(account["registration_inferred"], true); + assert_eq!(account["consent_version"], latest.consent_version); + assert_eq!(account["attribution_rule"], "current_or_last_external_30d"); + assert_eq!(account["lookback_days"], 30); + + let owner: i64 = sqlx::query_scalar("SELECT user_id FROM acquisition_visitors WHERE id=$1") + .bind(&visitor) + .fetch_one(&fixture.pg) + .await + .unwrap(); + let consent: (bool, i64) = + sqlx::query_as("SELECT allowed,version FROM acquisition_consents WHERE user_id=7") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(owner, 7); + assert_eq!(consent, (true, 2)); + + fixture + .store + .attribute_registration(7, &visitor) + .await + .unwrap(); + let account_count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM acquisition_accounts WHERE user_id=7") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(account_count, 1, "registration replay must stay idempotent"); + fixture.cleanup().await; +} + +#[tokio::test] +#[ignore = "requires isolated PostgreSQL via LMM_TEST_DATABASE_URL"] +async fn postgres_registration_cannot_override_explicit_consent_denial() { + let fixture = PgFixture::new().await; + fixture.store.withdraw(None, 7).await.unwrap(); + let registered_at: i64 = + sqlx::query_scalar("SELECT EXTRACT(EPOCH FROM clock_timestamp())::BIGINT + 10") + .fetch_one(&fixture.pg) + .await + .unwrap(); + sqlx::query("UPDATE users SET created_at=$1 WHERE id=7") + .bind(registered_at) + .execute(&fixture.pg) + .await + .unwrap(); + + let visitor = "c".repeat(64); + fixture + .store + .observe( + &visitor, + 0, + &input(json!({ + "consent":true, + "consent_version":2, + "nonce":"44444444444444444444444444444444", + "landing":"/sign-up", + "source":"community" + })), + &["api.lmm.best"], + ) + .await + .unwrap(); + fixture + .store + .attribute_registration(7, &visitor) + .await + .unwrap(); + + let account_count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM acquisition_accounts WHERE user_id=7") + .fetch_one(&fixture.pg) + .await + .unwrap(); + let consent: (bool, i64) = + sqlx::query_as("SELECT allowed,version FROM acquisition_consents WHERE user_id=7") + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!(account_count, 0, "denied users must not retain attribution"); + assert_eq!(consent, (false, 2)); + + let denied = fixture + .store + .observe( + &visitor, + 7, + &input(json!({ + "consent":true, + "consent_version":2, + "nonce":"55555555555555555555555555555555", + "landing":"/pricing", + "source":"documentation" + })), + &["api.lmm.best"], + ) + .await; + assert!(matches!(denied, Err(Error::Invalid(_)))); + fixture.cleanup().await; +} diff --git a/apps/api-rust/tests/scripts/check-real-integration-gates.sh b/apps/api-rust/tests/scripts/check-real-integration-gates.sh old mode 100755 new mode 100644 index ed5c66fd2..e85b31c8a --- a/apps/api-rust/tests/scripts/check-real-integration-gates.sh +++ b/apps/api-rust/tests/scripts/check-real-integration-gates.sh @@ -20,6 +20,7 @@ declare -A requirements=( [scripts.rs]='repository_options_commit_refresh_runtime_invalidate_cache_and_redact_audit|LMM_TEST_DATABASE_URL|LMM_AUTH_TEST_VALKEY_URL' [ai_directory.rs]='postgres_cache_and_audit_failures_do_not_reverse_committed_wallet_changes,postgres_create_replay_quote_changes_and_concurrency_charge_once,postgres_hide_refunds_once_and_wallet_failure_rolls_back_visibility,postgres_public_private_pagination_expiry_and_http_contract|LMM_TEST_DATABASE_URL|LMM_AUTH_TEST_VALKEY_URL' [token_queries.rs]='configured_token_prices_match_current_go_reference_live_maps_and_limits,persisted_usage_is_exact_token_scoped_utc_and_never_changes_credentials,quota_query_auth_checks_exact_key_expiry_owner_oauth_and_ip_without_status_writes,quota_query_limiter_is_shared_per_owner_across_keys_and_instances,token_pricing_checks_permissions_before_query_validation_and_never_mutates_key,token_pricing_uses_shared_credited_trust_facts_and_excludes_internal_credits|LMM_TEST_DATABASE_URL|LMM_AUTH_TEST_VALKEY_URL' + [acquisition.rs]='postgres_consent_visit_report_and_withdrawal_round_trip,postgres_link_policy_and_lookback_contracts_are_durable,postgres_registration_cannot_override_explicit_consent_denial,postgres_registration_keeps_first_touch_and_selects_last_external_visit|LMM_TEST_DATABASE_URL|' ) total_ignored=0 @@ -142,7 +143,7 @@ if rg -U -n 'else\s*\{\s*return;\s*\}' \ exit 1 fi -for suite in auth models api-token system-config migration announcements epay stripe catalog token-queries shared-trust token-cache relay-settlement scripts channel-balance; do +for suite in auth models api-token system-config migration announcements epay stripe catalog token-queries acquisition shared-trust token-cache relay-settlement scripts channel-balance; do if env -u LMM_TEST_DATABASE_URL -u LMM_AUTH_TEST_ALLOW_SCHEMA_RESET -u LMM_AUTH_TEST_DATABASE_URL -u LMM_AUTH_TEST_VALKEY_URL \ -u LMM_MODELS_TEST_DATABASE_URL -u LMM_MODELS_TEST_VALKEY_URL \ -u LMM_API_TOKEN_TEST_DATABASE_URL -u LMM_API_TOKEN_TEST_VALKEY_URL \ @@ -185,7 +186,7 @@ rg -Fq 'required integration test is missing:' "$empty_test_bin/announcements-ou # These suites must reject an empty COMPILED inventory before running Go, # opening the database, or treating libtest's zero-test success as a pass. -for suite in epay stripe catalog token-queries relay-settlement scripts; do +for suite in epay stripe catalog token-queries acquisition relay-settlement scripts; do if PATH="$empty_test_bin:$PATH" \ LMM_TEST_DATABASE_URL='postgresql://127.0.0.1:5432/isolated' \ LMM_AUTH_TEST_VALKEY_URL='redis://:fixture@127.0.0.1:6379/0' \ diff --git a/apps/api-rust/tests/scripts/run-real-integration-gates.sh b/apps/api-rust/tests/scripts/run-real-integration-gates.sh old mode 100755 new mode 100644 index dc318aaba..15c30dd45 --- a/apps/api-rust/tests/scripts/run-real-integration-gates.sh +++ b/apps/api-rust/tests/scripts/run-real-integration-gates.sh @@ -13,7 +13,7 @@ cd "$repo_root/apps/api-rust" suite=${1:-all} usage() { - echo "usage: $0 {auth|models|api-token|subscription-reset|migration|announcements|epay|stripe|catalog|token-queries|shared-trust|token-cache|relay-settlement|scripts|system-config|relay-timeouts|channel-balance|all}" >&2 + echo "usage: $0 {auth|models|api-token|subscription-reset|migration|announcements|epay|stripe|catalog|token-queries|acquisition|shared-trust|token-cache|relay-settlement|scripts|system-config|relay-timeouts|channel-balance|all}" >&2 exit 2 } @@ -191,6 +191,12 @@ run_token_queries() ( run_counted_api_integration_tests token_queries 6 ) +run_acquisition() { + require_loopback_url LMM_TEST_DATABASE_URL + require_api_ignored_test_count acquisition 4 + run_counted_api_integration_tests acquisition 4 +} + run_shared_trust() { require_loopback_url LMM_TEST_DATABASE_URL run_exact_api_lib_test auth::postgres::trust_pg_tests::current_go_credit_history_drives_dashboard_trust_access_and_refund_transitions @@ -370,6 +376,7 @@ case "$suite" in stripe) run_stripe ;; catalog) run_catalog ;; token-queries) run_token_queries ;; + acquisition) run_acquisition ;; shared-trust) run_shared_trust ;; token-cache) run_token_cache ;; relay-settlement) run_relay_settlement ;; @@ -377,6 +384,6 @@ case "$suite" in system-config) run_system_config ;; relay-timeouts) run_relay_timeouts ;; channel-balance) run_channel_balance ;; - all) run_auth; run_models; run_api_token; run_subscription_reset; run_system_config; run_migration; run_announcements; run_epay; run_stripe; run_catalog; run_token_queries; run_shared_trust; run_token_cache; run_relay_settlement; run_scripts; run_relay_timeouts; run_channel_balance ;; + all) run_auth; run_models; run_api_token; run_subscription_reset; run_system_config; run_migration; run_announcements; run_epay; run_stripe; run_catalog; run_token_queries; run_acquisition; run_shared_trust; run_token_cache; run_relay_settlement; run_scripts; run_relay_timeouts; run_channel_balance ;; *) usage ;; esac diff --git a/apps/api-rust/tests/scripts/test-new-integration-suites.py b/apps/api-rust/tests/scripts/test-new-integration-suites.py index f2c25efdf..20e4e14d6 100644 --- a/apps/api-rust/tests/scripts/test-new-integration-suites.py +++ b/apps/api-rust/tests/scripts/test-new-integration-suites.py @@ -10,7 +10,7 @@ SCRIPTS = Path(__file__).resolve().parent RUNNER = SCRIPTS / "run-real-integration-gates.sh" -SUITES = {"epay":"epay_runtime_postgres", "stripe":"epay_runtime_postgres", "catalog":"ai_directory", "token-queries":"token_queries", "relay-settlement":"relay_openai_settlement_pg", "scripts":"scripts", "shared-trust":"lib", "token-cache":"lib"} +SUITES = {"epay":"epay_runtime_postgres", "stripe":"epay_runtime_postgres", "catalog":"ai_directory", "token-queries":"token_queries", "acquisition":"acquisition", "relay-settlement":"relay_openai_settlement_pg", "scripts":"scripts", "shared-trust":"lib", "token-cache":"lib"} ORACLES = ("LMM_EPAY_GO_ORACLE_OUTPUT", "LMM_STRIPE_GO_ORACLE_OUTPUT", "LMM_STRIPE_SUBSCRIPTION_GO_ORACLE_OUTPUT", "LMM_STRIPE_SUBSCRIPTION_CHECKOUT_GO_ORACLE_OUTPUT", "LMM_AI_DIRECTORY_GO_ORACLE_OUTPUT", "LMM_TOKEN_PRICING_GO_ORACLE_OUTPUT", "LMM_RELAY_FUNDING_GO_VECTORS", "LMM_RELAY_PRICE_GO_VECTORS") FAKE_CARGO = r'''#!/usr/bin/env python3 @@ -197,7 +197,7 @@ def test_skipped_or_incomplete_go_export_never_reaches_rust_execution(self): def test_remote_dependencies_and_aliases_are_rejected_before_any_tool(self): common = (("LMM_TEST_DATABASE_URL","postgresql://fixture:fixture@example.com:5432/production"),("LMM_AUTH_TEST_VALKEY_URL","redis://:fixture@example.com:6379/0")) for suite in SUITES: - variables = list(common if suite != "shared-trust" else common[:1]) + variables = list(common[:1] if suite in ("shared-trust", "acquisition") else common) if suite in ("epay","stripe"): variables += [("LMM_EPAY_TEST_DATABASE_URL",common[0][1]),("LMM_EPAY_TEST_VALKEY_URL",common[1][1])] if suite == "relay-settlement": variables += [("LMM_API_TOKEN_TEST_VALKEY_URL",common[1][1])] From 4aa25772586a486a55909f94ad3f8e6e4a3d5ac6 Mon Sep 17 00:00:00 2001 From: LIghtJUNction Date: Wed, 30 Sep 2026 15:27:43 +0800 Subject: [PATCH 2/2] test(rust): lock acquisition time boundaries and replay snapshot (#548) --- apps/api-rust/tests/acquisition.rs | 144 ++++++++++++++++-- .../scripts/check-real-integration-gates.sh | 2 +- .../scripts/run-real-integration-gates.sh | 4 +- 3 files changed, 136 insertions(+), 14 deletions(-) diff --git a/apps/api-rust/tests/acquisition.rs b/apps/api-rust/tests/acquisition.rs index 86f032f35..d6429460c 100644 --- a/apps/api-rust/tests/acquisition.rs +++ b/apps/api-rust/tests/acquisition.rs @@ -153,6 +153,16 @@ impl PgFixture { } } + async fn set_visit_time(&self, visit: i64, created_at: i64) { + let result = sqlx::query("UPDATE acquisition_visits SET created_at=$1 WHERE id=$2") + .bind(created_at) + .bind(visit) + .execute(&self.pg) + .await + .unwrap(); + assert_eq!(result.rows_affected(), 1); + } + async fn cleanup(self) { self.pg.close().await; sqlx::query(&format!("DROP SCHEMA {} CASCADE", self.schema)) @@ -320,11 +330,7 @@ async fn postgres_consent_visit_report_and_withdrawal_round_trip() { #[ignore = "requires isolated PostgreSQL via LMM_TEST_DATABASE_URL"] async fn postgres_registration_keeps_first_touch_and_selects_last_external_visit() { let fixture = PgFixture::new().await; - let registered_at: i64 = - sqlx::query_scalar("SELECT EXTRACT(EPOCH FROM clock_timestamp())::BIGINT + 10") - .fetch_one(&fixture.pg) - .await - .unwrap(); + let registered_at: i64 = 1_800_000_000; sqlx::query("UPDATE users SET created_at=$1 WHERE id=7") .bind(registered_at) .execute(&fixture.pg) @@ -382,6 +388,13 @@ async fn postgres_registration_keeps_first_touch_and_selects_last_external_visit .await .unwrap(); + // Explicit fixture timestamps keep ordering independent of runner speed. + fixture.set_visit_time(first.id, registered_at - 100).await; + fixture + .set_visit_time(selected.id, registered_at - 50) + .await; + fixture.set_visit_time(latest.id, registered_at - 10).await; + fixture .store .attribute_registration(7, &visitor) @@ -418,6 +431,27 @@ async fn postgres_registration_keeps_first_touch_and_selects_last_external_visit assert_eq!(owner, 7); assert_eq!(consent, (true, 2)); + // A newly received visit would win a fresh attribution calculation. + // Replaying registration must preserve the original persisted snapshot. + let later = fixture + .store + .observe( + &visitor, + 7, + &input(json!({ + "consent":true, + "consent_version":2, + "nonce":"66666666666666666666666666666666", + "landing":"/guide", + "source":"documentation", + "campaign":"late-arrival" + })), + &["api.lmm.best"], + ) + .await + .unwrap(); + fixture.set_visit_time(later.id, registered_at - 1).await; + fixture .store .attribute_registration(7, &visitor) @@ -429,6 +463,16 @@ async fn postgres_registration_keeps_first_touch_and_selects_last_external_visit .await .unwrap(); assert_eq!(account_count, 1, "registration replay must stay idempotent"); + let replayed: Value = sqlx::query_scalar( + "SELECT to_jsonb(acquisition_accounts) FROM acquisition_accounts WHERE user_id=7", + ) + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!( + replayed, account, + "replay must preserve every attribution field" + ); fixture.cleanup().await; } @@ -437,11 +481,7 @@ async fn postgres_registration_keeps_first_touch_and_selects_last_external_visit async fn postgres_registration_cannot_override_explicit_consent_denial() { let fixture = PgFixture::new().await; fixture.store.withdraw(None, 7).await.unwrap(); - let registered_at: i64 = - sqlx::query_scalar("SELECT EXTRACT(EPOCH FROM clock_timestamp())::BIGINT + 10") - .fetch_one(&fixture.pg) - .await - .unwrap(); + let registered_at: i64 = 1_800_000_000; sqlx::query("UPDATE users SET created_at=$1 WHERE id=7") .bind(registered_at) .execute(&fixture.pg) @@ -449,7 +489,7 @@ async fn postgres_registration_cannot_override_explicit_consent_denial() { .unwrap(); let visitor = "c".repeat(64); - fixture + let visit = fixture .store .observe( &visitor, @@ -465,6 +505,7 @@ async fn postgres_registration_cannot_override_explicit_consent_denial() { ) .await .unwrap(); + fixture.set_visit_time(visit.id, registered_at - 1).await; fixture .store .attribute_registration(7, &visitor) @@ -502,3 +543,84 @@ async fn postgres_registration_cannot_override_explicit_consent_denial() { assert!(matches!(denied, Err(Error::Invalid(_)))); fixture.cleanup().await; } + +#[tokio::test] +#[ignore = "requires isolated PostgreSQL via LMM_TEST_DATABASE_URL"] +async fn postgres_registration_respects_visit_time_boundaries() { + const DAY: i64 = 86_400; + let registered_at: i64 = 1_800_000_000; + // Each case has only one candidate, so a missing bound cannot be masked + // by another, more recent visit. Both lower bounds are inclusive. + for (offset, has_first, has_registration) in [ + (-90 * DAY - 1, false, false), + (-90 * DAY, true, false), + (-30 * DAY - 1, true, false), + (-30 * DAY, true, true), + (-30 * DAY + 1, true, true), + (0, true, true), + (1, false, false), + ] { + let fixture = PgFixture::new().await; + sqlx::query("UPDATE users SET created_at=$1 WHERE id=7") + .bind(registered_at) + .execute(&fixture.pg) + .await + .unwrap(); + let visitor = "d".repeat(64); + let visit = fixture + .store + .observe( + &visitor, + 0, + &input(json!({ + "consent":true, + "consent_version":2, + "nonce":"77777777777777777777777777777777", + "landing":"/pricing", + "source":"community" + })), + &["api.lmm.best"], + ) + .await + .unwrap(); + fixture + .set_visit_time(visit.id, registered_at + offset) + .await; + fixture + .store + .attribute_registration(7, &visitor) + .await + .unwrap(); + let account: Value = sqlx::query_scalar( + "SELECT to_jsonb(acquisition_accounts) FROM acquisition_accounts WHERE user_id=7", + ) + .fetch_one(&fixture.pg) + .await + .unwrap(); + assert_eq!( + account["first_visit_id"], + if has_first { visit.id } else { 0 }, + "first-touch boundary at offset {offset}" + ); + assert_eq!( + account["registration_visit_id"], + if has_registration { visit.id } else { 0 }, + "registration boundary at offset {offset}" + ); + assert_eq!( + account["registration_source"], + if has_registration { + "community" + } else { + "unknown" + }, + "registration source at offset {offset}" + ); + assert_eq!( + account["consent_version"], + if offset <= 0 { 2 } else { 0 }, + "post-registration consent must not leak backwards at offset {offset}" + ); + fixture.cleanup().await; + } +} diff --git a/apps/api-rust/tests/scripts/check-real-integration-gates.sh b/apps/api-rust/tests/scripts/check-real-integration-gates.sh index e85b31c8a..7743b1f10 100644 --- a/apps/api-rust/tests/scripts/check-real-integration-gates.sh +++ b/apps/api-rust/tests/scripts/check-real-integration-gates.sh @@ -20,7 +20,7 @@ declare -A requirements=( [scripts.rs]='repository_options_commit_refresh_runtime_invalidate_cache_and_redact_audit|LMM_TEST_DATABASE_URL|LMM_AUTH_TEST_VALKEY_URL' [ai_directory.rs]='postgres_cache_and_audit_failures_do_not_reverse_committed_wallet_changes,postgres_create_replay_quote_changes_and_concurrency_charge_once,postgres_hide_refunds_once_and_wallet_failure_rolls_back_visibility,postgres_public_private_pagination_expiry_and_http_contract|LMM_TEST_DATABASE_URL|LMM_AUTH_TEST_VALKEY_URL' [token_queries.rs]='configured_token_prices_match_current_go_reference_live_maps_and_limits,persisted_usage_is_exact_token_scoped_utc_and_never_changes_credentials,quota_query_auth_checks_exact_key_expiry_owner_oauth_and_ip_without_status_writes,quota_query_limiter_is_shared_per_owner_across_keys_and_instances,token_pricing_checks_permissions_before_query_validation_and_never_mutates_key,token_pricing_uses_shared_credited_trust_facts_and_excludes_internal_credits|LMM_TEST_DATABASE_URL|LMM_AUTH_TEST_VALKEY_URL' - [acquisition.rs]='postgres_consent_visit_report_and_withdrawal_round_trip,postgres_link_policy_and_lookback_contracts_are_durable,postgres_registration_cannot_override_explicit_consent_denial,postgres_registration_keeps_first_touch_and_selects_last_external_visit|LMM_TEST_DATABASE_URL|' + [acquisition.rs]='postgres_consent_visit_report_and_withdrawal_round_trip,postgres_link_policy_and_lookback_contracts_are_durable,postgres_registration_cannot_override_explicit_consent_denial,postgres_registration_keeps_first_touch_and_selects_last_external_visit,postgres_registration_respects_visit_time_boundaries|LMM_TEST_DATABASE_URL|' ) total_ignored=0 diff --git a/apps/api-rust/tests/scripts/run-real-integration-gates.sh b/apps/api-rust/tests/scripts/run-real-integration-gates.sh index 15c30dd45..42b4a6008 100644 --- a/apps/api-rust/tests/scripts/run-real-integration-gates.sh +++ b/apps/api-rust/tests/scripts/run-real-integration-gates.sh @@ -193,8 +193,8 @@ run_token_queries() ( run_acquisition() { require_loopback_url LMM_TEST_DATABASE_URL - require_api_ignored_test_count acquisition 4 - run_counted_api_integration_tests acquisition 4 + require_api_ignored_test_count acquisition 5 + run_counted_api_integration_tests acquisition 5 } run_shared_trust() {