653 lines
23 KiB
Rust
653 lines
23 KiB
Rust
use axum::{
|
|
body::Body,
|
|
http::{Request, StatusCode, header},
|
|
};
|
|
use fingerprint_receiver::{app, db, geoip::GeoIpService};
|
|
use http_body_util::BodyExt;
|
|
use serde_json::json;
|
|
use tempfile::tempdir;
|
|
use tower::ServiceExt;
|
|
|
|
async fn response_json(response: axum::response::Response) -> serde_json::Value {
|
|
let bytes = response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.expect("body should be readable")
|
|
.to_bytes();
|
|
serde_json::from_slice(&bytes).expect("response should be JSON")
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn network_probe_reports_observed_connection_without_storing_stats() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/network/probe")
|
|
.header("x-forwarded-for", "203.0.113.42")
|
|
.header("cf-connecting-ip", "198.51.100.24")
|
|
.header("cf-ipcountry", "US")
|
|
.header("cf-ray", "abc123-LAX")
|
|
.header("x-forwarded-proto", "https")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = response_json(response).await;
|
|
assert_eq!(body["observedIp"], serde_json::Value::Null);
|
|
assert_eq!(body["observedPort"], serde_json::Value::Null);
|
|
assert_eq!(body["forwardedFor"], "203.0.113.42");
|
|
assert_eq!(body["cfConnectingIp"], "198.51.100.24");
|
|
assert_eq!(body["cfIpCountry"], "US");
|
|
assert_eq!(body["cfRay"], "abc123-LAX");
|
|
assert_eq!(body["forwardedProto"], "https");
|
|
assert_eq!(body["geoIp"]["city"]["available"], false);
|
|
assert_eq!(body["geoIp"]["asn"]["available"], false);
|
|
assert!(body["geoIp"]["sourceIp"].is_string());
|
|
assert_eq!(body["stored"], false);
|
|
|
|
let stats_response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/stats")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let stats = response_json(stats_response).await;
|
|
assert_eq!(stats["totalScans"], 0);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn post_scan_accepts_expanded_profile_signal_set() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
let signals: Vec<_> = (0..32)
|
|
.map(|index| {
|
|
json!({
|
|
"id": format!("profile-signal-{index}"),
|
|
"weight": 0,
|
|
"score": 0.0,
|
|
"contribution": 0,
|
|
"raw": format!("stage=passive category=summary index={index}")
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(
|
|
json!({
|
|
"score": 44,
|
|
"band": "medium",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"signals": signals
|
|
})
|
|
.to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
|
|
let stats_response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/stats")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let stats = response_json(stats_response).await;
|
|
assert_eq!(stats["totalScans"], 1);
|
|
assert_eq!(stats["bandCounts"]["medium"], 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn analytics_returns_public_profile_distributions() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
|
|
for payload in [
|
|
json!({
|
|
"score": 72,
|
|
"band": "high",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"timezoneOffsetMinutes": -480,
|
|
"userAgentFamily": "Chrome",
|
|
"signals": [
|
|
{"id": "timezone", "weight": 30, "score": 1.0, "contribution": 30, "raw": "Asia/Shanghai"},
|
|
{"id": "language", "weight": 24, "score": 1.0, "contribution": 24, "raw": "zh-CN"},
|
|
{"id": "profile-local-summary", "weight": 0, "score": 1.0, "contribution": 0, "raw": "stage=local facts=12 low=4 medium=5 high=2 critical=1"},
|
|
{"id": "profile-local-categories", "weight": 0, "score": 0.0, "contribution": 0, "raw": "stage=local categories=screen=2 locale=3 hardware=4"},
|
|
{"id": "network-webrtc", "weight": 0, "score": 1.0, "contribution": 0, "raw": "webrtc total=3 public=1 private=1 mdns=1 host=2 srflx=1 relay=0"},
|
|
{"id": "network-server", "weight": 0, "score": 1.0, "contribution": 0, "raw": "server observed=1 forwarded=1 cf=0 real=0"},
|
|
{"id": "network-connection", "weight": 0, "score": 0.0, "contribution": 0, "raw": "connection api=1 type=4g saveData=0 rtt=40"}
|
|
]
|
|
}),
|
|
json!({
|
|
"score": 38,
|
|
"band": "medium",
|
|
"locale": "en-US",
|
|
"timezone": "America/Los_Angeles",
|
|
"timezoneOffsetMinutes": 420,
|
|
"userAgentFamily": "Safari",
|
|
"signals": [
|
|
{"id": "language", "weight": 24, "score": 0.0, "contribution": 0, "raw": "en-US"},
|
|
{"id": "profile-request-summary", "weight": 0, "score": 0.75, "contribution": 0, "raw": "stage=request facts=6 low=2 medium=2 high=2 critical=0"},
|
|
{"id": "profile-request-categories", "weight": 0, "score": 0.0, "contribution": 0, "raw": "stage=request categories=request=2 network=4"},
|
|
{"id": "network-webrtc", "weight": 0, "score": 1.0, "contribution": 0, "raw": "webrtc total=1 public=0 private=1 mdns=0 host=1 srflx=0 relay=0"},
|
|
{"id": "network-server", "weight": 0, "score": 1.0, "contribution": 0, "raw": "server observed=0 forwarded=0 cf=1 real=1"},
|
|
{"id": "network-connection", "weight": 0, "score": 0.0, "contribution": 0, "raw": "connection api=1 type=3g saveData=1 rtt=250"}
|
|
]
|
|
}),
|
|
] {
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(payload.to_string()))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
}
|
|
|
|
let response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/analytics")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
let body = response_json(response).await;
|
|
assert_eq!(body["totalScans"], 2);
|
|
assert_eq!(body["bandCounts"]["high"], 1);
|
|
assert_eq!(body["localeCounts"][0]["label"], "en-US");
|
|
assert_eq!(body["timezoneCounts"][0]["count"], 1);
|
|
assert_eq!(body["userAgentCounts"][0]["label"], "Chrome");
|
|
assert_eq!(
|
|
body["scoreBuckets"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|bucket| bucket["label"] == "61-80")
|
|
.unwrap()["count"],
|
|
1
|
|
);
|
|
assert_eq!(
|
|
body["timezoneOffsetCounts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|bucket| bucket["label"] == "UTC+08:00")
|
|
.unwrap()["count"],
|
|
1
|
|
);
|
|
assert_eq!(
|
|
body["stageCounts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|bucket| bucket["label"] == "request")
|
|
.unwrap()["count"],
|
|
1
|
|
);
|
|
assert_eq!(
|
|
body["categoryCounts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|bucket| bucket["label"] == "network")
|
|
.unwrap()["count"],
|
|
4
|
|
);
|
|
assert_eq!(body["sensitivityTotals"]["low"], 6);
|
|
assert_eq!(body["sensitivityTotals"]["medium"], 7);
|
|
assert_eq!(body["sensitivityTotals"]["high"], 4);
|
|
assert_eq!(body["sensitivityTotals"]["critical"], 1);
|
|
assert_eq!(body["networkExposure"]["webrtcSamples"], 2);
|
|
assert_eq!(body["networkExposure"]["webrtcCandidates"], 4);
|
|
assert_eq!(body["networkExposure"]["webrtcPublic"], 1);
|
|
assert_eq!(body["networkExposure"]["webrtcPrivate"], 2);
|
|
assert_eq!(body["networkExposure"]["webrtcMdns"], 1);
|
|
assert_eq!(body["networkExposure"]["serverSamples"], 2);
|
|
assert_eq!(body["networkExposure"]["serverObserved"], 1);
|
|
assert_eq!(body["networkExposure"]["forwardedFor"], 1);
|
|
assert_eq!(body["networkExposure"]["cfConnectingIp"], 1);
|
|
assert_eq!(body["networkExposure"]["realIpHeader"], 1);
|
|
assert_eq!(body["networkExposure"]["connectionSamples"], 2);
|
|
assert_eq!(body["networkExposure"]["saveData"], 1);
|
|
assert_eq!(body["networkExposure"]["averageRttMs"], 145.0);
|
|
assert_eq!(
|
|
body["connectionTypeCounts"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|bucket| bucket["label"] == "4g")
|
|
.unwrap()["count"],
|
|
1
|
|
);
|
|
assert!(body["updatedAt"].as_str().is_some());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn post_scan_persists_event_and_updates_stats() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
|
|
let payload = json!({
|
|
"score": 62,
|
|
"band": "high",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"timezoneOffsetMinutes": -480,
|
|
"userAgentFamily": "Apple",
|
|
"signals": [
|
|
{
|
|
"id": "timezone",
|
|
"weight": 30,
|
|
"score": 1.0,
|
|
"contribution": 30,
|
|
"raw": "Asia/Shanghai"
|
|
},
|
|
{
|
|
"id": "language",
|
|
"weight": 24,
|
|
"score": 1.0,
|
|
"contribution": 24,
|
|
"raw": "zh-CN, en-US"
|
|
}
|
|
]
|
|
});
|
|
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(payload.to_string()))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
let body = response_json(response).await;
|
|
assert_eq!(body["stored"], true);
|
|
assert!(body["id"].as_str().is_some_and(|id| id.len() > 20));
|
|
|
|
let stats_response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/stats")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(stats_response.status(), StatusCode::OK);
|
|
let stats = response_json(stats_response).await;
|
|
assert_eq!(stats["totalScans"], 1);
|
|
assert_eq!(stats["bandCounts"]["high"], 1);
|
|
assert_eq!(stats["averageScore"], 62.0);
|
|
assert_eq!(stats["topSignals"][0]["id"], "timezone");
|
|
assert_eq!(stats["topSignals"][0]["hits"], 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn post_scan_returns_tracking_history_for_repeated_fingerprint() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
let payload = json!({
|
|
"score": 62,
|
|
"band": "high",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"signals": [],
|
|
"tracking": {
|
|
"visitorKey": "visitor-alpha",
|
|
"fingerprintHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"components": [
|
|
{"id": "timezone", "hash": "1111111111111111111111111111111111111111111111111111111111111111"},
|
|
{"id": "canvas-hash", "hash": "2222222222222222222222222222222222222222222222222222222222222222"}
|
|
]
|
|
}
|
|
});
|
|
|
|
let first = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(payload.to_string()))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(first.status(), StatusCode::CREATED);
|
|
let first_body = response_json(first).await;
|
|
assert_eq!(first_body["tracking"]["visitNumber"], 1);
|
|
assert_eq!(first_body["tracking"]["exactMatchCount"], 0);
|
|
assert_eq!(first_body["tracking"]["verdict"], "first_visit");
|
|
|
|
let second = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(payload.to_string()))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(second.status(), StatusCode::CREATED);
|
|
let second_body = response_json(second).await;
|
|
assert_eq!(second_body["tracking"]["visitNumber"], 2);
|
|
assert_eq!(second_body["tracking"]["exactMatchCount"], 1);
|
|
assert_eq!(second_body["tracking"]["fingerprintChanged"], false);
|
|
assert_eq!(second_body["tracking"]["verdict"], "exact_repeat");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn post_scan_estimates_masking_effectiveness_for_changed_components() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
|
|
for payload in [
|
|
json!({
|
|
"score": 62,
|
|
"band": "high",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"signals": [],
|
|
"tracking": {
|
|
"visitorKey": "visitor-mask",
|
|
"fingerprintHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
|
"components": [
|
|
{"id": "timezone", "hash": "1111111111111111111111111111111111111111111111111111111111111111"},
|
|
{"id": "platform", "hash": "2222222222222222222222222222222222222222222222222222222222222222"},
|
|
{"id": "canvas-hash", "hash": "3333333333333333333333333333333333333333333333333333333333333333"}
|
|
]
|
|
}
|
|
}),
|
|
json!({
|
|
"score": 62,
|
|
"band": "high",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"signals": [],
|
|
"tracking": {
|
|
"visitorKey": "visitor-mask",
|
|
"fingerprintHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
|
"components": [
|
|
{"id": "timezone", "hash": "9999999999999999999999999999999999999999999999999999999999999999"},
|
|
{"id": "platform", "hash": "8888888888888888888888888888888888888888888888888888888888888888"},
|
|
{"id": "canvas-hash", "hash": "7777777777777777777777777777777777777777777777777777777777777777"}
|
|
]
|
|
}
|
|
}),
|
|
] {
|
|
let response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(payload.to_string()))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
}
|
|
|
|
let third = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(
|
|
json!({
|
|
"score": 62,
|
|
"band": "high",
|
|
"locale": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"signals": [],
|
|
"tracking": {
|
|
"visitorKey": "visitor-mask",
|
|
"fingerprintHash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
|
"components": [
|
|
{"id": "timezone", "hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
|
|
{"id": "platform", "hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},
|
|
{"id": "canvas-hash", "hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"}
|
|
]
|
|
}
|
|
})
|
|
.to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(third.status(), StatusCode::CREATED);
|
|
let body = response_json(third).await;
|
|
assert_eq!(body["tracking"]["visitNumber"], 3);
|
|
assert_eq!(body["tracking"]["fingerprintChanged"], true);
|
|
assert_eq!(body["tracking"]["stableComponents"], 0);
|
|
assert_eq!(body["tracking"]["changedComponents"], 3);
|
|
assert_eq!(body["tracking"]["verdict"], "mask_likely_effective");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn post_scan_rejects_invalid_tracking_hash() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
|
|
let response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(
|
|
json!({
|
|
"score": 62,
|
|
"band": "high",
|
|
"signals": [],
|
|
"tracking": {
|
|
"visitorKey": "visitor-alpha",
|
|
"fingerprintHash": "raw ip 127.0.0.1",
|
|
"components": []
|
|
}
|
|
})
|
|
.to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
let body = response_json(response).await;
|
|
assert_eq!(body["error"], "validation_failed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn post_scan_rejects_invalid_score() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let app = app::router_with_geoip(pool, GeoIpService::disabled());
|
|
|
|
let response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("POST")
|
|
.uri("/api/scan")
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(
|
|
json!({
|
|
"score": 130,
|
|
"band": "high",
|
|
"signals": []
|
|
})
|
|
.to_string(),
|
|
))
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
let body = response_json(response).await;
|
|
assert_eq!(body["error"], "validation_failed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn rust_router_serves_frontend_dist_with_spa_fallback() {
|
|
let pool = db::connect("sqlite::memory:").await.expect("db opens");
|
|
db::migrate(&pool).await.expect("schema migrates");
|
|
let dist = tempdir().expect("temp dist exists");
|
|
std::fs::create_dir_all(dist.path().join("assets")).expect("assets dir can be created");
|
|
std::fs::write(
|
|
dist.path().join("index.html"),
|
|
"<main id=\"root\">shell</main>",
|
|
)
|
|
.expect("index can be written");
|
|
std::fs::write(
|
|
dist.path().join("assets").join("bundle.js"),
|
|
"console.log('bundle');",
|
|
)
|
|
.expect("asset can be written");
|
|
|
|
let app = app::router_with_assets(pool, dist.path());
|
|
|
|
let index_response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(index_response.status(), StatusCode::OK);
|
|
assert!(
|
|
index_response
|
|
.headers()
|
|
.get(header::CONTENT_TYPE)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.starts_with("text/html")
|
|
);
|
|
let index_body = index_response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.unwrap()
|
|
.to_bytes();
|
|
assert!(String::from_utf8_lossy(&index_body).contains("shell"));
|
|
|
|
let deep_link_response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/scan/history")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(deep_link_response.status(), StatusCode::OK);
|
|
let deep_link_body = deep_link_response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.unwrap()
|
|
.to_bytes();
|
|
assert!(String::from_utf8_lossy(&deep_link_body).contains("shell"));
|
|
|
|
let asset_response = app
|
|
.clone()
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/assets/bundle.js")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(asset_response.status(), StatusCode::OK);
|
|
let asset_body = asset_response
|
|
.into_body()
|
|
.collect()
|
|
.await
|
|
.unwrap()
|
|
.to_bytes();
|
|
assert_eq!(&asset_body[..], b"console.log('bundle');");
|
|
|
|
let api_404_response = app
|
|
.oneshot(
|
|
Request::builder()
|
|
.method("GET")
|
|
.uri("/api/missing")
|
|
.body(Body::empty())
|
|
.unwrap(),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(api_404_response.status(), StatusCode::NOT_FOUND);
|
|
}
|