Mục lục
- Mục Tiêu Bài Học
- Setup Test Harness Cho Middleware Test
- Test 1: CORS Preflight Layer
- Test 2: Security Headers Layer
- Test 3: Body Limit + Validation Layer
- Test 4: Rate Limit + Trace + Metrics Layer
- Test 5: Timeout + Panic Catch Layer
- Test 6: Fallback 404 + 405 + Uniform Envelope
- Tổng Kết Group 8 + Roadmap G9-G15
- Tổng Kết
- Bài Tập Củng Cố
- Bài Tiếp Theo
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Setup integration test harness cho middleware stack tái sử dụng
TestContextB75. - Test 11 layer order end-to-end qua testcontainers Postgres isolated.
- Verify layer interaction: CORS preflight, security headers, rate limit, body limit, trace, metrics, timeout, handler, fallback, panic catch.
- Pattern assert middleware execution order qua side-effect (metrics counter, log capture, header check).
- Hoàn thành Group 8 (10/10) — tổng kết stack hardening + observability + transport-layer concern.
- Roadmap Group 9-15 — foundation cho 70 bài tiếp theo (JWT Auth, RBAC, User Management, Cart Advanced, Inventory, Admin Dashboard, Observability).
- File path lock: NEW
crates/shop-api/tests/middleware_test.rs+ extendtests/common/mod.rsvớiassert_envelopehelper.
Setup Test Harness Cho Middleware Test
B75 đã lock pattern TestContext spawn 1 container Postgres riêng cho mỗi #[tokio::test] qua testcontainers, build full router production (kèm 11 layer middleware) qua build_app(state), giữ container alive trong struct để tránh drop sớm. B85 tái sử dụng nguyên harness đó, chỉ bổ sung 1 helper assert envelope dùng chung cho 6 test class trong bài:
// File: crates/shop-api/tests/common/mod.rs (extend B75 lock)
use serde_json::Value;
pub struct TestContext {
pub app: axum::Router,
pub pool: sqlx::PgPool,
_container: testcontainers::ContainerAsync<
testcontainers_modules::postgres::Postgres,
>,
}
impl TestContext {
pub async fn new() -> Self {
// B75 lock: spawn Postgres container + migrate + build router production
// ... giữ nguyên implementation B75
}
}
/// Helper assert envelope chuẩn Shop API.
/// Lock B16 + B48 + B84 — mọi error response PHẢI có 3 field tối thiểu.
pub fn assert_envelope(body: &Value, expected_code: &str) {
assert!(
body.get("error").is_some(),
"missing 'error' field, body = {body}"
);
assert_eq!(
body["code"].as_str().expect("'code' must be string"),
expected_code,
"code mismatch, body = {body}"
);
assert!(
body.get("request_id").is_some(),
"missing 'request_id' field, body = {body}"
);
}
Helper assert_envelope là điểm xoay của bài: B16 lock envelope {error, code, request_id, detail?} cho mọi 5xx/4xx response, B84 lock 4 path uniform (handler + fallback + timeout + panic) — verify chéo cả 6 test class chỉ cần 1 hàm.
Helper request(app, method, uri, body) đã có sẵn B75 (auto-inject Idempotency-Key UUID v4 cho POST/PATCH/DELETE B66 lock, parse body sang serde_json::Value, cap 1MB). B85 tái sử dụng nguyên signature.
Test 1: CORS Preflight Layer
Test đầu tiên verify CorsLayer B77 chặn OPTIONS preflight trước router, trả 204 No Content + 4-5 header CORS chuẩn fetch spec:
// File: crates/shop-api/tests/middleware_test.rs
use axum::{body::Body, http::{Request, StatusCode}};
use tower::ServiceExt;
#[tokio::test]
async fn test_cors_preflight() {
let ctx = TestContext::new().await;
let req = Request::builder()
.method("OPTIONS")
.uri("/api/v1/products")
.header("Origin", "https://frontend.blogcode.vn")
.header("Access-Control-Request-Method", "POST")
.header("Access-Control-Request-Headers", "Content-Type, Authorization")
.body(Body::empty())
.unwrap();
let response = ctx.app.clone().oneshot(req).await.unwrap();
// CorsLayer trả 204 No Content cho preflight
assert_eq!(response.status(), StatusCode::NO_CONTENT);
let headers = response.headers();
assert!(headers.contains_key("Access-Control-Allow-Origin"));
assert!(headers.contains_key("Access-Control-Allow-Methods"));
assert!(headers.contains_key("Access-Control-Allow-Headers"));
assert_eq!(
headers.get("Access-Control-Max-Age").unwrap(),
"86400",
);
}
Verify đầy đủ B77 lock: CorsLayer nhận OPTIONS có Origin + Access-Control-Request-Method → chặn trước router → 204 + 4 header (Allow-Origin từ allowlist B77, Allow-Methods từ config, Allow-Headers từ config, Max-Age: 86400 cache browser preflight 1 ngày). Handler method_not_allowed KHÔNG bị gọi vì CorsLayer chặn trước — behavior chuẩn fetch spec.
Test 2: Security Headers Layer
Test thứ hai verify SetResponseHeaderLayer B77 tự inject 6 OWASP security header trên mọi response success. Bug regression dễ xảy ra khi refactor router strip nhầm layer — test này lock 6 header MUST present:
#[tokio::test]
async fn test_security_headers() {
let ctx = TestContext::new().await;
let req = Request::builder()
.method("GET")
.uri("/api/v1/products")
.body(Body::empty())
.unwrap();
let response = ctx.app.clone().oneshot(req).await.unwrap();
let headers = response.headers();
// 6 OWASP security header lock B77
assert_eq!(headers.get("X-Frame-Options").unwrap(), "DENY");
assert_eq!(headers.get("X-Content-Type-Options").unwrap(), "nosniff");
assert!(
headers
.get("Strict-Transport-Security")
.unwrap()
.to_str()
.unwrap()
.contains("max-age=31536000"),
);
assert!(headers.get("Content-Security-Policy").is_some());
assert_eq!(
headers.get("Referrer-Policy").unwrap(),
"strict-origin-when-cross-origin",
);
assert!(headers.get("Permissions-Policy").is_some());
}
6 header được map sang 6 OWASP Top 10 mitigation: X-Frame-Options: DENY chặn clickjacking A05, X-Content-Type-Options: nosniff chặn MIME sniffing A05, Strict-Transport-Security max-age=31536000 ép HTTPS 1 năm A02, Content-Security-Policy chặn XSS A03, Referrer-Policy strict-origin-when-cross-origin bảo vệ privacy, Permissions-Policy giới hạn browser API. Pattern lock: thêm middleware mới mà strip nhầm 1 trong 6 header sẽ làm test này fail ngay tại CI.
Test 3: Body Limit + Validation Layer
Test thứ ba verify 2 layer phòng thủ tầng nhập liệu: body limit B79 (reject payload lớn từ outer layer trước khi parse JSON) và validation B83 (reject field lạ qua deny_unknown_fields):
#[tokio::test]
async fn test_body_limit_reject() {
let ctx = TestContext::new().await;
// 3MB body vượt limit 2MB default
let big_body = "x".repeat(3 * 1024 * 1024);
let req = Request::builder()
.method("POST")
.uri("/api/v1/products")
.header("Content-Type", "application/json")
.body(Body::from(big_body))
.unwrap();
let response = ctx.app.clone().oneshot(req).await.unwrap();
// Outer RequestBodyLimitLayer 413 hoặc DefaultBodyLimit 400 — tùy layer nào chặn trước
assert!(
matches!(response.status().as_u16(), 400 | 413),
"expected 400 or 413, got {}",
response.status(),
);
}
#[tokio::test]
async fn test_validation_unknown_field() {
let ctx = TestContext::new().await;
let (status, body) = request(
&ctx.app,
"POST",
"/api/v1/products",
Some(json!({
"name": "Sample",
"slug": "sample",
"price": "100.00",
"stock": 1,
"typo_field": "oops",
})),
)
.await;
assert_eq!(status, StatusCode::BAD_REQUEST);
assert!(
body["error"].as_str().unwrap().contains("unknown field"),
"body = {body}",
);
}
Body limit B79 wire 2 tầng: outer RequestBodyLimitLayer reject 413 trước khi đọc bytes, inner DefaultBodyLimit::max(N) per-route 400 (axum khái niệm body limit nội bộ extractor). Test này tolerate cả 2 status để không phụ thuộc thứ tự apply chính xác giữa các build (cả 2 đều là refuse hợp lệ). Validation B83 verify #[serde(deny_unknown_fields)] MANDATORY trên mọi Create/Update DTO — client gửi typo typo_field → reject 400 + body chứa "unknown field" giúp client debug nhanh thay vì silent ignore.
Test 4: Rate Limit + Trace + Metrics Layer
Test thứ tư verify rate limit B78 trigger 429 khi vượt ngưỡng và metrics endpoint B81 serve format Prometheus đúng:
#[tokio::test]
async fn test_rate_limit_429() {
let ctx = TestContext::new().await;
// POST /users/register lock 60/min per IP B78
let request_count = 70;
let mut last_status = StatusCode::OK;
for i in 0..request_count {
let (status, _) = request(
&ctx.app,
"POST",
"/api/v1/users/register",
Some(json!({
"email": format!("test{i}@example.com"),
"password": "Pass1234!",
"display_name": "X",
})),
)
.await;
last_status = status;
}
// Sau 60 request trong 1 phút → 429
assert_eq!(last_status, StatusCode::TOO_MANY_REQUESTS);
}
#[tokio::test]
async fn test_metrics_emit() {
let ctx = TestContext::new().await;
// Phát 5 request để increment counter
for _ in 0..5 {
let _ = request(&ctx.app, "GET", "/api/v1/products", None).await;
}
let (status, body) = request(&ctx.app, "GET", "/metrics", None).await;
assert_eq!(status, StatusCode::OK);
// body là plain text Prometheus format (không phải JSON)
// Bỏ qua assertion JSON, chỉ verify endpoint sống + status 200
let _ = body;
}
Rate limit B78 lock 60 request/phút per-IP cho /api/v1/users/register (endpoint dễ bị brute-force tạo account spam) qua tower-governor — request 61-70 trả 429 + header Retry-After. Test gửi tuần tự 70 request → request thứ 70 chắc chắn 429. Metrics endpoint B81 trả text/plain; version=0.0.4 format Prometheus (counter, histogram, gauge) — không parse được sang JSON nên chỉ verify status 200. Endpoint /metrics không qua middleware rate limit để Prometheus scrape không bị throttle.
Pitfall đáng chú ý: rate limit có state per-IP trong tower-governor in-memory store; 2 test cùng TestContext dùng IP 127.0.0.1 sẽ chia state → flaky. Pattern B75 spawn container + Router instance riêng per test giải quyết: mỗi test có rate limiter mới, state reset hoàn toàn — không cần manual cleanup.
Test 5: Timeout + Panic Catch Layer
Test thứ năm cần 2 endpoint giả lập /test/slow + /test/panic mount conditional compile (đã có sẵn từ B84 lock #[cfg(debug_assertions)]). Test build (cargo test) chạy debug profile mặc định → 2 endpoint sống → ngoài production binary release vẫn ẩn hoàn toàn:
// File: crates/shop-api/src/routes/test_panic.rs (đã có B84)
#[cfg(debug_assertions)]
async fn panic_handler() -> &'static str {
panic!("oops something went wrong");
}
#[cfg(debug_assertions)]
async fn slow_handler() -> &'static str {
// Sleep 10s vượt default timeout 5s B82
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
"should not reach here"
}
#[cfg(debug_assertions)]
pub fn routes() -> axum::Router<crate::state::AppState> {
use axum::routing::get;
axum::Router::new()
.route("/test/panic", get(panic_handler))
.route("/test/slow", get(slow_handler))
}
Test class assert 2 layer cuối stack: TimeoutLayer B82 + CatchPanicLayer B84:
#[tokio::test]
async fn test_timeout_504() {
let ctx = TestContext::new().await;
let req = Request::builder()
.method("GET")
.uri("/test/slow")
.body(Body::empty())
.unwrap();
let response = ctx.app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::GATEWAY_TIMEOUT);
let body_bytes = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_envelope(&body, "REQUEST_TIMEOUT");
}
#[tokio::test]
async fn test_panic_catch_500() {
let ctx = TestContext::new().await;
let req = Request::builder()
.method("GET")
.uri("/test/panic")
.body(Body::empty())
.unwrap();
let response = ctx.app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let body_bytes = axum::body::to_bytes(response.into_body(), 1024).await.unwrap();
let body: Value = serde_json::from_slice(&body_bytes).unwrap();
assert_envelope(&body, "INTERNAL_ERROR");
// OWASP A05: panic message KHÔNG được expose trong response B84
let error_text = body["error"].as_str().unwrap();
assert!(
!error_text.contains("panic") && !error_text.contains("oops"),
"response leaked panic message: {error_text}",
);
}
Verify chéo: timeout B82 trả 504 + envelope code REQUEST_TIMEOUT + detail timeout_seconds (helper assert_envelope chỉ verify 3 field tối thiểu, detail kiểm tra riêng nếu cần). Panic catch B84 trả 500 + envelope code INTERNAL_ERROR + body masked KHÔNG chứa panic message — assertion chéo verify OWASP A05 prevention (panic "oops something went wrong" chỉ vào server log qua tracing::error!, không leak response client).
Test 6: Fallback 404 + 405 + Uniform Envelope
Test class cuối là điểm chốt Group 8: verify uniform envelope MANDATORY 4 path consistent (handler error + fallback + timeout + panic) — bug dễ miss khi refactor 1 path mà quên path khác:
#[tokio::test]
async fn test_404_fallback() {
let ctx = TestContext::new().await;
let (status, body) = request(
&ctx.app,
"GET",
"/api/v1/non-existing-route",
None,
)
.await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert_envelope(&body, "NOT_FOUND");
}
#[tokio::test]
async fn test_405_method_not_allowed() {
let ctx = TestContext::new().await;
// /products chỉ define GET + POST, gửi DELETE → 405
let req = Request::builder()
.method("DELETE")
.uri("/api/v1/products")
.body(Body::empty())
.unwrap();
let response = ctx.app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
// axum tự sinh Allow header — KHÔNG cần manual
assert!(response.headers().contains_key("Allow"));
}
#[tokio::test]
async fn test_uniform_envelope_4_paths() {
let ctx = TestContext::new().await;
// Path 1: Handler error (422 validation B41)
let (_, body_handler) = request(
&ctx.app,
"POST",
"/api/v1/products",
Some(json!({})),
)
.await;
assert!(body_handler.get("error").is_some());
assert!(body_handler.get("code").is_some());
assert!(body_handler.get("request_id").is_some());
// Path 2: Fallback 404 (B84)
let (_, body_fallback) = request(&ctx.app, "GET", "/not-exist", None).await;
assert!(body_fallback.get("error").is_some());
assert!(body_fallback.get("code").is_some());
assert!(body_fallback.get("request_id").is_some());
// Path 3: Timeout 504 (B82)
let (_, body_timeout) = request(&ctx.app, "GET", "/test/slow", None).await;
assert!(body_timeout.get("error").is_some());
assert!(body_timeout.get("code").is_some());
assert!(body_timeout.get("request_id").is_some());
// Path 4: Panic 500 (B84)
let (_, body_panic) = request(&ctx.app, "GET", "/test/panic", None).await;
assert!(body_panic.get("error").is_some());
assert!(body_panic.get("code").is_some());
assert!(body_panic.get("request_id").is_some());
}
Test test_uniform_envelope_4_paths là integration test most-valuable của Group 8 — single test catch bug regression của bất kỳ path nào trong 4 path uniform envelope: B16 handler error, B84 fallback, B82 timeout, B84 panic. Client SDK chỉ cần 1 đoạn code parse cho mọi error → bất kỳ path nào silently miss 1 field sẽ làm client throw undefined.error tại runtime → test này phải pass tuyệt đối trên CI mọi PR.
Chạy test full suite + expected output:
$ cd shop && cargo test --test middleware_test --workspace
running 10 tests
test test_cors_preflight ... ok
test test_security_headers ... ok
test test_body_limit_reject ... ok
test test_validation_unknown_field ... ok
test test_rate_limit_429 ... ok
test test_metrics_emit ... ok
test test_timeout_504 ... ok
test test_panic_catch_500 ... ok
test test_404_fallback ... ok
test test_405_method_not_allowed ... ok
test test_uniform_envelope_4_paths ... ok
test result: ok. 11 passed; 0 failed; 0 ignored; finished in 23.42s
Container Postgres mỗi test ~3-5s spawn — 11 test parallel với --test-threads=4 giảm tổng còn ~25s; sequential sẽ 40-55s. CI dùng service container Postgres dùng chung B58 Pattern 1 thay testcontainers Pattern 2 cho local — pipeline pre-merge giữ tốc độ ổn định.
Tổng Kết Group 8 + Roadmap G9-G15
Group 8 Middleware Sâu đã cover trọn 10 bài B76-B85 — mỗi bài thêm 1 layer hoặc 1 pattern hardening lock vĩnh viễn cho Shop API:
- B76 middleware overview +
tower::Service/Layertrait foundation + ordering bottom-up. - B77 CORS allowlist + 6 OWASP security header.
- B78 rate limit per-IP + per-user qua tower-governor.
- B79 body limit 4 tầng phòng thủ DoS payload.
- B80 tracing + structured log JSON multi-env (Local pretty, Staging/Production JSON).
- B81 metrics Prometheus + 4 metric type (counter/histogram/gauge/summary) + cardinality control.
- B82 timeout per-route 4 class + drop guard cleanup.
- B83 validation + sanitize + 4 cross-cutting rule (deny_unknown_fields, regex slug, html_escape, truncate_for_log).
- B84 fallback 404/405 + panic catch + uniform envelope 3 entry point error.
- B85 middleware integration test 11 layer + 6 test class verify chéo.
Stack 11 layer production-grade lock vĩnh viễn (outermost → innermost, đọc code từ dưới lên):
// File: crates/shop-api/src/router.rs (sau B85)
Router::new()
.merge(routes::health::routes())
.route("/metrics", get(routes::metrics::metrics))
.nest("/api/v1", api_v1)
.fallback(handlers::fallback::not_found)
.method_not_allowed_fallback(handlers::fallback::method_not_allowed)
.with_state(state)
// INNER 11 — panic_catch (innermost, sát handler nhất) B84
.layer(panic_catch_layer())
// INNER 10 — validation extractor (handler-level qua ValidatedJson) B83
// INNER 9 — timeout per-route B82
// INNER 8 — body_limit per-route B79
// INNER 7 — rate_limit per-route B78
// INNER 6 — enrich_error (inject request_id vào 4xx/5xx body) B39
// INNER 5 — request_id (set X-Request-Id header) B39
// INNER 4 — security_headers (6 OWASP header) B77
// INNER 3 — cors (allowlist + preflight) B77
// INNER 2 — metrics_layer (3 metric per-request) B81
// INNER 1 — trace_layer (span + structured log) B80
// OUTERMOST 2 — decompression (gzip/br request body) B50
// OUTERMOST 1 — compression (gzip/br response body) B50
Foundation cho 70 bài tiếp theo G9-G15 đã sẵn sàng. Mỗi feature mới future (JWT Auth B86, RBAC B96, User Management B106, Cart Advanced B116, Inventory B126, Admin Dashboard B136, Observability sâu B146) chỉ cần plug vào 11 layer này:
- G9 Auth + JWT (B86-B95): JWT structure HS256/RS256, jsonwebtoken crate, refresh token rotation, blacklist Redis, key rotation JWKS.
- G10 Role-Based Access Control (B96-B105): user-role-permission schema, Casbin enforcer, route guard middleware tích hợp 11 layer.
- G11 User Management Sâu (B106-B115): registration flow, email verify, password reset, address book, preferences, soft delete + GDPR.
- G12 Cart + Order Advanced (B116-B125): cart merge guest→user, checkout flow distributed lock, order state machine, refund.
- G13 Inventory + Notification (B126-B135): stock reservation, restock job, email notification (apalis worker), push notification real-time.
- G14 Admin Dashboard (B136-B145): analytics endpoint, audit log search, bulk action, export report CSV/XLSX.
- G15 Observability + Background Worker (B146-B155): OpenTelemetry collector, Grafana dashboard, alert rule PromQL, apalis-redis background queue.
Snapshot Shop API sau khi đóng Group 8: 5 crate workspace (shop-api + shop-common + shop-db + shop-core + shop-macros), 30+ endpoint sống cho 7 resource (products + orders + cart + users + categories + brands + payments), 14 table + 33+ index + 15 migration applied, 22 AppError variant, 11 middleware layer production-grade, integration test pattern lock testcontainers + tower::oneshot + 4 path uniform envelope verify.
Tổng Kết
- 6 test class Group 8 verification: CORS preflight + security headers + body limit/validation + rate limit/metrics + timeout/panic + fallback uniform envelope.
testcontainers + tower::oneshotpattern (B75 lock continued) — fast, isolated, parallel safe.assert_envelopehelper verify uniform shape{error, code, request_id}consistent mọi error path.- Test 4 path consistency: handler + fallback + timeout + panic — single test catch regression.
- Layer ordering verify qua side-effect (header check, metric counter, log capture) — không assert internal state private.
- Stack 11 layer production-grade đầy đủ sau Group 8 — lock vĩnh viễn.
- HOÀN THÀNH Group 8 (10/10) — middleware hardening + observability + transport-layer concern cover trọn.
- Foundation cho G9-G15 roadmap 70 bài tiếp theo (JWT, RBAC, User Mgmt, Cart, Inventory, Admin, Observability).
- File path lock: NEW
crates/shop-api/tests/middleware_test.rs+ extendtests/common/mod.rsvớiassert_envelopehelper shared. - Pattern test 6 layer + reuse test endpoint giả lập
/test/slow+/test/panicconditional compile#[cfg(debug_assertions)]KHÔNG ship production binary release.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Integration test 11 layer — pros/cons so với unit test per layer?
mockallmock service vs real testcontainers — khi nào dùng cái nào? - 4 path uniform envelope test — tại sao MANDATORY cover cả 4? Cho ví dụ scenario bug client parse fail khi 1 path miss field.
- Rate limit test 60/min — flaky test scenario nếu chạy liên tục? Solution reset state.
- Test endpoint giả lập
/test/slow+/test/panic— pros/cons compile-time conditional#[cfg(debug_assertions)]vs always-on. Pattern an toàn nhất production. - 11 layer ordering verify — assert sequence qua side-effect. Cho ví dụ pattern observable middleware effect (header, metric, log) để verify layer chạy đúng thứ tự.
Đáp án
- Integration test 11 layer pros/cons: Pros: (a) verify code production thực sự chạy (không phải mock), bug middleware ordering hoặc side-effect chéo middleware-handler chỉ catch được khi run full stack — vd middleware
enrich_errorB39 wrap response saupanic_catchB84 → unit test panic_catch isolated thấy body envelope chuẩn nhưng integration test thấyrequest_idbị inject 2 lần do enrich_error parse rồi rebuild; (b) catch regression khi thêm/xóa layer mới — vd thêm AuthLayer G9 sai vị trí (outer hơn rate_limit) sẽ làm unauth request vẫn tốn DB lookup → integration test rate_limit fail; (c) verify uniform envelope 4 path B84 chỉ test integration mới làm được vì panic catch + fallback + timeout không trigger trong unit test handler isolated. Cons: (a) chậm 3-5s/test vs ms unit test → không chạy mỗi keystroke dev; (b) cần Docker → onboarding tốn 1 bước cài; (c) khó debug khi fail (nhiều layer chồng, log mixed) → cần good observability test runner. mockall vs testcontainers phân vai trò: mockall (B72) dùng cho pure business logic 1 service isolated (latency < 50ms, không cần Docker, chạy mỗi PR push) — verify logic conditional, edge case (input validation, branching) không cần qua middleware/DB; testcontainers (B75 + B85) dùng cho full stack verify middleware + handler + service + DB + side-effect chéo (latency 3-5s/test, cần Docker, chạy pre-merge CHỈ) — verify production behavior. Pattern lock Shop API: cả 2 cùng tồn tại, không thay thế nhau — pyramid base mock test (lượng nhiều, run nhanh) + integration test top (lượng ít, run pre-merge). - 4 path uniform envelope MANDATORY tại sao: 4 path produce error trong Shop API là (a) handler error (
AppError IntoResponseB16), (b) fallback 404/405 (B84), (c) timeout 504 (B82), (d) panic 500 (B84). Client SDK design pattern là 1 đoạn code parse cho mọi error — vd JSconst { error, code, request_id } = await response.json(); toast.error(t(code)). Nếu 1 trong 4 path miss fieldcodehoặcrequest_id, client thử access.codesẽundefined→t(undefined)trả empty string → toast trắng → user không biết bị lỗi gì. Scenario bug client parse fail cụ thể: timeout B82 ban đầu wireHandleErrorLayerkhông gắnhandle_timeoutcustom handler → tower default trả body plain text"request timeout"không phải JSON. Frontend gọiresponse.json()trên text này →SyntaxError: Unexpected token 'r' in JSON→ toast "Lỗi không xác định" thay vì "Yêu cầu hết hạn, vui lòng thử lại". Support team nhận 50 ticket "không gửi được đơn", debug rất khó vì frontend không capture đúng error. Testtest_uniform_envelope_4_pathscatch trường hợp này tại CI trước khi merge. Pattern lock: integration test verify 4 path cùng 1 test → 1 hàm fail = ngay lập tức biết regression path nào, không cần debug nhiều test. - Rate limit flaky test scenario: rate limit tower-governor giữ state in-memory per process qua
GovernorLayer. Nếu 2 test cùngcargo testchia router instance (vdlazy_static APP: Router = build_app()singleton) sẽ chia rate limiter → test thứ nhất tiêu 60 request → test thứ hai chạy ngay sau đó vẫn dính 429 không hoàn lại bucket. Solution reset state: (a) B75 pattern: mỗi test gọiTestContext::new()spawn router instance riêng → rate limiter mới fresh — không cần manual reset, drop tự sạch; (b) Manual reset: nếu phải chia router (vd test E2E full workflow chain nhiều endpoint), expose methodgovernor.clear()hoặc initGovernorConfig::with_clock(MockClock)control time → advance clock 1 phút giữa 2 test reset window; (c) Different IP per test:X-Forwarded-Forheader inject giả IP khác nhau mỗi test → tower-governor bucket riêng — nhưng cần overrideSecureClientIpSourceconfig trust X-Forwarded-For trong test, KHÔNG production (security pitfall A07 broken auth). Shop API B85 chọn (a) — đơn giản nhất + zero state cross-test. #[cfg(debug_assertions)]pros/cons: Pros: (a) endpoint test giả lập KHÔNG vào binary release (cargo build --releasestrip toàn bộcfg(debug_assertions)) — production binary tuyệt đối không có/test/panichoặc/test/slowđể attacker probe; (b) dev local + CI test chạy debug profile mặc định → endpoint sống → integration test pass; (c) zero overhead production — compile-time gate không phải runtime check (vsif config.env == "local"runtime check tốn 1 nano-second + dependency config). Cons: (a) staging test mà build release sẽ KHÔNG có endpoint giả lập → manual smoke test không reproduce được — cần build debug riêng cho staging hoặc dùng#[cfg(any(debug_assertions, feature = "test-endpoints"))]+ flagcargo build --release --features test-endpoints; (b) khó on-call debug production khi panic xảy ra thật vì không thể trigger panic test endpoint check pipeline — dùng synthetic monitoring tool external (vd Datadog Synthetic) trigger lỗi giả lập network timeout thay vì panic. Always-on alternative:/test/*endpoint mount sau auth middleware require roleinternal-test+ IP allowlist 10.0.0.0/8 (network internal) → vẫn ship production nhưng inaccessible external. Trade-off: complexity tăng + nguy cơ misconfigured (role thiếu enforce → leak). Shop API B84 chọncfg(debug_assertions)đơn giản + an toàn nhất; nếu cần test endpoint trên staging future thì migrate sang feature flag.- 11 layer ordering verify qua side-effect: Rust middleware là
tower::Servicegeneric — không cách nào assert internal state ordering qua reflection. Pattern lock: verify observable side-effect mỗi layer phát ra → suy ngược ordering. 5 pattern observable cụ thể: (a) Header check: middlewareSetResponseHeaderLayerB77 inject 6 OWASP header → test response chứa header → layer chạy;X-Request-IdB39 inject header → test response có header → request_id middleware chạy trước handler. (b) Metric counter: metrics_layer B81 emithttp_requests_total{method,path,status}mỗi request → test request 5 lần, query/metricscounter += 5 → layer chạy; advanced: counter increment sau handler error 500 vs success 200 verify ordering metric layer outer hơn handler. (c) Log capture: trace_layer B80 emit span "request" với field method/uri/status/latency → test setuptracing-testsubscriber capture log → assert log entry tồn tại → layer chạy; advanced: log entry chứaerrorfield khi status 5xx verify on_failure callback chạy. (d) Status code transformation: timeout_layer B82 transform handler timeout panic → 504 + body envelopeREQUEST_TIMEOUT; rate_limit B78 transform request 61 → 429 + headerRetry-After. (e) Body envelope mutation: enrich_error B39 injectrequest_idfield vào body 4xx/5xx → test verify body chứa request_id (test này MANDATORY uniform envelope B85). Pitfall: không nên assert internal state private (vdrate_limiter.bucket_count) vì sẽ vỡ test khi refactor library — chỉ assert public observable behavior (header, metric endpoint, log line, body content). Pattern lock Shop API: 5 observable above MANDATORY cho mọi middleware mới Group 9-15 phải có ít nhất 1 observable side-effect verify được integration test, nếu không phải refactor expose helper test.
Bài Tiếp Theo
Bài 86: JWT Auth Overview — Mở Group 9 — mở Group 9 Auth: JWT vs Session, JWT structure (header + payload + signature), algorithm HS256 vs RS256, library jsonwebtoken crate, design Shop API auth flow: register → login → refresh.
