Trang chủ
QA Learning Hub
Dự Án Thực Hành · QuickBite Mobile App
Thực Hành Full Project

QuickBite — Food Delivery Mobile App

Dự án thực hành đầy đủ từ A đến Z theo chuẩn quốc tế — Requirements, Test Plan, Test Cases (Frontend + Backend), Bug Reports, Performance, Security và Go/No-Go Report.

📱 Project Overview
📌 Project Brief
QuickBite là ứng dụng đặt đồ ăn cho thị trường Đông Nam Á. Sprint 3 (2 weeks) focus vào User Authentication + Restaurant Browse + Cart & Checkout. Team: 4 devs (2 mobile, 2 backend), 1 QA (bạn), 1 PM, 1 Designer.
ItemDetail
App NameQuickBite v1.0.0 — Sprint 3 Build
PlatformsiOS 16+ (iPhone X và mới hơn), Android 11+ (API level 30+)
FrontendReact Native 0.73 + TypeScript
BackendNode.js 20 + Express 4 + TypeScript
DatabasePostgreSQL 15 (primary), Redis 7 (session + cache)
CloudAWS (ECS Fargate, RDS, ElastiCache, S3, CloudFront)
AuthJWT (access 15min) + Refresh Token (7 days), OAuth2 (Google, Apple Sign-in)
PaymentStripe (cards), GrabPay, MoMo (Vietnam)
Sprint GoalUser có thể đăng ký, đăng nhập, browse nhà hàng, thêm món vào giỏ và checkout thành công
Test Environmentstaging.quickbite.app (cloned prod infra, seeded data)
CI/CDGitHub Actions → AWS CodeDeploy
📋 Requirements
🏗️ Architecture Review
📝 Test Plan
✅ Test Cases
🧪 Execution
🐛 Bug Reports
📊 Report
🚦 Go/No-Go
🏗️ System Architecture — QA Perspective
Client
React Native App
iOS + Android. State management: Redux Toolkit. Navigation: React Navigation 6. HTTP client: Axios với interceptors.
API Gateway
AWS API Gateway
Rate limiting: 100 req/min per IP. Throttle: 10 req/sec. WAF rules chặn OWASP Top 10.
Backend
REST API (Node.js)
5 service modules: Auth, User, Restaurant, Order, Payment. Swagger docs tại /api-docs. Versioning: /api/v1/
Database
PostgreSQL + Redis
PG: user, restaurant, menu, order, payment tables. Redis: session tokens, cart state, restaurant cache (TTL 5min).
External
Third-party Services
Stripe (payment), Firebase (push notifications), Google Maps (restaurant locations), Twilio (OTP SMS).
Monitoring
Observability Stack
DataDog APM, CloudWatch logs, Sentry (errors), PagerDuty (on-call). QA monitors Sentry sau mỗi deploy.
💡 QA Integration Points — Cần Test Kỹ
1. App ↔ API Gateway: JWT validation, rate limiting behavior.
2. API ↔ PostgreSQL: Transaction integrity, concurrent writes (race condition khi 2 users order món cuối cùng).
3. API ↔ Redis: Cache invalidation khi restaurant update menu.
4. API ↔ Stripe: Webhook handling khi payment success/fail.
5. App ↔ Firebase: Push notification sau khi order confirmed.
Development
dev.quickbite.app
Docker Compose local. Mock payment. Reset daily. Dev only.
Staging
staging.quickbite.app
AWS clone. Stripe test mode. Seeded 50 restaurants. QA + Dev access.
Production
app.quickbite.com
Live users. Post-deploy smoke only. QA read-only access.
📋 User Stories & Acceptance Criteria

Sprint 3 scope: 5 Epics, 12 User Stories. QA review đánh dấu AC quality theo tiêu chí CCCTF (Completeness, Clarity, Consistency, Testability, Feasibility).

Epic 1: User Authentication

US-001 Đăng ký tài khoản bằng email + password 5
"As a new user, I want to create an account with email and password so that I can use the QuickBite service."
Acceptance Criteria
  • AC-001: Form có 3 fields: Full Name (required, 2-100 chars), Email (required, valid format), Password (required, min 8 chars, có uppercase + lowercase + number)
  • AC-002: Khi submit thành công → app navigate tới OTP Verification screen
  • AC-003: OTP gửi qua email trong vòng 60 giây, có hiệu lực 10 phút
  • AC-004: Email đã tồn tại → hiện error "Email này đã được đăng ký. Đăng nhập?"
  • AC-005: Password không đủ mạnh → hiện real-time inline error message
  • AC-006: Register button disabled khi có validation error
  • AC-007: Sau khi verify OTP thành công → tạo JWT + refresh token, navigate tới Home screen
US-002 Đăng nhập bằng email + password 3
"As a registered user, I want to log in with my email and password so that I can access my account."
  • AC-001: Sai password lần 1-4 → hiện error, counter giảm "N lần thử còn lại"
  • AC-002: Sai password lần 5 → lock account 15 phút, hiện thông báo
  • AC-003: Đăng nhập thành công → navigate Home, user data loaded
  • AC-004: "Nhớ đăng nhập" (Remember Me) → refresh token lưu securely (Keychain iOS / Keystore Android)
  • AC-005: Unverified account → hiện prompt "Chưa xác thực email. Gửi lại OTP?"
US-003 Đăng nhập bằng Google / Apple 3
"As a user, I want to sign in with my Google or Apple account for a faster onboarding experience."
  • AC-001: "Continue with Google" → Google OAuth flow → auto-create account nếu email mới
  • AC-002: "Continue with Apple" → Apple Sign-in → yêu cầu trên iOS 13+ (Apple guideline)
  • AC-003: Google/Apple email đã linked với password account → merge, không tạo duplicate
  • AC-004: OAuth thất bại (user cancel, no network) → hiện error message, ở lại Login screen

Epic 2: Restaurant Browse

US-004 Xem danh sách nhà hàng theo vị trí 5
"As a logged-in user, I want to see restaurants near my location so that I can choose where to order from."
  • AC-001: App request location permission. Nếu granted → dùng GPS. Nếu denied → prompt nhập địa chỉ tay
  • AC-002: API trả về restaurants trong vòng 5km, sort by distance by default
  • AC-003: Mỗi card hiển thị: tên, ảnh, rating (1 decimal), thời gian giao (phút), phí giao hàng, status (Mở/Đóng)
  • AC-004: Restaurant "Đóng cửa" vẫn hiển thị nhưng có badge "Đóng" và không thể order
  • AC-005: API response cache 5 phút (Redis). Pull-to-refresh force reload từ DB
  • AC-006: Empty state khi không có nhà hàng trong 5km: "Chưa có nhà hàng tại khu vực của bạn"
  • AC-007: Loading skeleton UI khi đang fetch data (không flash empty screen)
US-005 Tìm kiếm nhà hàng và món ăn 3
  • AC-001: Search debounce 300ms — không call API mỗi keystroke
  • AC-002: Tìm kiếm theo tên nhà hàng VÀ tên món ăn (full-text search PostgreSQL)
  • AC-003: Min 2 ký tự để trigger search
  • AC-004: "Không tìm thấy kết quả cho '...' " → suggest popular restaurants
  • AC-005: Recent searches lưu local (max 10 items), có thể xóa từng item

Epic 3: Cart & Checkout

US-006 Thêm/xóa món ăn vào giỏ hàng 3
  • AC-001: Cart chỉ chứa món từ 1 nhà hàng. Thêm món nhà hàng khác → alert "Xóa giỏ hàng cũ?"
  • AC-002: Tăng/giảm số lượng (+ / −), min = 1. Số lượng 0 = xóa khỏi cart
  • AC-003: Cart state persist khi đóng app (AsyncStorage + server-side cart)
  • AC-004: Nếu món hết hàng sau khi thêm vào cart → hiện badge "Hết hàng" khi mở cart, không cho checkout
  • AC-005: Subtotal tự động tính lại khi thay đổi quantity
  • AC-006: Max 20 items per cart (business rule)
US-007 Checkout và thanh toán 8
"As a user, I want to complete my order and pay securely so that my food will be delivered."
  • AC-001: Checkout screen hiển thị: items list, subtotal, delivery fee, discount, total
  • AC-002: User chọn địa chỉ giao hàng (saved addresses hoặc nhập mới)
  • AC-003: Payment methods: Visa/MC/Amex card (via Stripe), GrabPay, MoMo, Cash on Delivery
  • AC-004: "Place Order" button disabled khi: không có địa chỉ, cart rỗng, restaurant đóng cửa
  • AC-005: Sau khi order thành công → Order Confirmation screen với order ID và estimated delivery time
  • AC-006: Push notification "Đơn hàng #QB-XXXX đã được xác nhận" trong 30 giây
  • AC-007: Nếu payment thất bại → order status "Payment Failed", user có thể retry
  • AC-008: Idempotency — double-click "Place Order" không tạo 2 orders
⚠️ QA Requirements Review — Issues Found
US-007 AC-006: "trong 30 giây" — không testable vì phụ thuộc Firebase delivery time. Đề xuất với BA: thay bằng "notification được trigger từ server trong 5 giây sau khi order confirmed."
US-004 AC-005: Cache 5 phút có thể conflict với AC-004 (restaurant đóng cửa). Nếu cache stale → user thấy nhà hàng mở nhưng thực ra đã đóng → checkout sẽ fail. Cần clarify behavior.
🗺️ Requirements Traceability Matrix (RTM)

RTM mapping giữa User Story AC → Test Cases → Execution Status. Sprint 3 scope.

Req ID Acceptance Criteria Test Case ID(s) Type Status
US-001AC-001: Form validationTC-AUTH-001 đến 005Manual + AutoPassed ✓
AC-002: Navigate to OTP screenTC-AUTH-006E2E AutoPassed ✓
AC-003: OTP delivery 60sTC-AUTH-007, 008Manual + APIPassed ✓
AC-004: Duplicate email errorTC-AUTH-009E2E AutoPassed ✓
AC-005: Password strength real-timeTC-AUTH-010, 011E2E AutoFailed ✗ BUG-001
AC-006: Button disabled stateTC-AUTH-012E2E AutoPassed ✓
AC-007: JWT created after OTP verifyTC-AUTH-013, 014API AutoPassed ✓
US-002AC-001: Failed attempt counterTC-AUTH-015, 016Manual + APIPassed ✓
AC-002: Account lock 15 minTC-AUTH-017API AutoPassed ✓
AC-004: Remember Me — secure storageTC-AUTH-020Manual (device)Passed ✓
US-003AC-001: Google OAuth flowTC-AUTH-021, 022ManualPassed ✓
AC-003: No duplicate on mergeTC-AUTH-024API AutoFailed ✗ BUG-002
US-004AC-002: Restaurants within 5kmTC-REST-001, 002API AutoPassed ✓
AC-003: Card data displayTC-REST-003 đến 007E2E AutoPassed ✓
AC-005: Cache 5 min + pull refreshTC-REST-010, 011API + ManualFailed ✗ BUG-003
US-007AC-008: Idempotency (no double order)TC-ORDER-015API AutoPassed ✓
AC-007: Payment failed → retryTC-ORDER-012, 013ManualPassed ✓
42
Total Test Cases
38
Executed
35
Passed
3
Failed
4
Not Executed
92%
Req Coverage
📝 Test Plan — Sprint 3
MụcNội Dung
Test ObjectivesVerify all Sprint 3 user stories meet acceptance criteria. Ensure no regressions in Auth module from Sprint 2. Validate API performance under 200 concurrent users.
In ScopeRegistration, Login (email + OAuth), OTP verification, Restaurant listing + search, Cart management, Checkout + payment (Stripe test mode)
Out of ScopeOrder tracking flow (Sprint 4), Admin dashboard, Driver app, Real payment transactions (staging uses Stripe test mode)
Test TypesFunctional (manual + automated E2E), API testing, Performance (load), Security (OWASP scan), Mobile-specific (device matrix)
Test EnvironmentsPrimary: staging.quickbite.app | Devices: iOS 16 (iPhone 14), iOS 17 (iPhone 15 Pro), Android 13 (Pixel 7), Android 14 (Samsung S24)
Entry CriteriaBuild deployed to staging ✓, Swagger API docs updated ✓, Dev unit tests passing (≥80% coverage) ✓, Test data seeded ✓
Exit Criteria≥90% test cases executed, Pass rate ≥88%, 0 Critical open bugs, All P1 bugs fixed and verified, RTM 100% complete
TimelineDay 1-2: Test design | Day 3-7: Manual + E2E execution | Day 8-9: Bug retest + regression | Day 10: Report + Go/No-Go
RisksStaging Stripe webhook may have 30s delay (vs production 5s) → mock webhook tests. OAuth test account may need 2FA bypass.
ToolsDetox (E2E mobile automation), Postman + Newman (API), k6 (performance), OWASP ZAP (security), Jira (bug tracking), Allure (reporting)
🏆 Senior QA Note
Test plan không phải là formality. Mỗi "Out of Scope" phải được PM sign-off. Mỗi "Risk" phải có mitigation. Exit criteria phải có con số cụ thể — "testing complete khi QA happy" không phải exit criteria.
📱 Frontend Test Cases — Mobile UI

Test cases cho React Native app. Automated bằng Detox (E2E) + manual verification trên thiết bị thật.

Authentication — TC-AUTH

TC IDTest CaseStepsExpectedPriorityResult
TC-AUTH-001 Register form — happy path Điền Name="John Doe", Email="john@test.com", Password="Test123!" → tap Register Navigate tới OTP screen, hiện "OTP đã gửi tới john@test.com" P1PASS
TC-AUTH-002 Register — password quá ngắn Nhập password "Test1!" (6 chars) → tap Register Inline error "Mật khẩu tối thiểu 8 ký tự" xuất hiện dưới field P1PASS
TC-AUTH-003 Register — password không có số Nhập password "TestPassword!" → focus out Inline error "Mật khẩu phải có ít nhất 1 chữ số" P1PASS
TC-AUTH-004 Register — email format invalid Nhập email "notanemail" → tap Register Inline error "Email không hợp lệ", Register button disabled P1PASS
TC-AUTH-005 Register — email đã tồn tại Đăng ký với email đã có trong DB Error message "Email này đã được đăng ký. Đăng nhập?" với link tới Login P1PASS
TC-AUTH-010 Real-time password strength indicator Gõ từng ký tự trong password field: "T" → "Te" → "Tes" → "Test" → "Test1" → "Test1!" → "Test1!A" Strength indicator cập nhật sau mỗi keystroke: Weak → Weak → Weak → Medium → Medium → Strong → Strong P2FAIL — BUG-001
TC-AUTH-017 Account lock sau 5 failed logins Login sai password 5 lần liên tiếp Sau lần 5: "Tài khoản tạm khóa 15 phút. Thử lại lúc 14:35" (hiện giờ cụ thể) P1PASS
TC-AUTH-018 Login — keyboard không đẩy UI lên quá mức (Android) Trên Android, tap email field → keyboard hiện Không bị keyboard che mất password field và Login button vẫn visible hoặc scrollable P2PASS

Restaurant Browse — TC-REST

TC IDTest CaseStepsExpectedPriorityResult
TC-REST-001 Home screen — restaurant list hiển thị đúng Đăng nhập → grant location permission → xem Home screen Danh sách nhà hàng trong 5km, sort by distance. Mỗi card có đủ: ảnh, tên, rating, thời gian, phí ship P1PASS
TC-REST-004 Restaurant "Đóng cửa" — không cho order Tap vào nhà hàng có status "Đóng" Badge "Đóng cửa" hiển thị. Tất cả menu item disabled. Không có "Add to Cart" button P1PASS
TC-REST-010 Pull-to-refresh bypass cache 1. Gọi API xem restaurant. 2. Trong vòng 5 phút, Pull-to-refresh. 3. Check Network tab → HTTP call có được gọi không Pull-to-refresh PHẢI trigger API call mới (bỏ qua Redis cache), server trả fresh data. Cache-Control header phải có no-cache khi force refresh. P1FAIL — BUG-003
TC-REST-012 Search debounce — không spam API Mở Network Monitor → Type "pho" nhanh, từng ký tự (< 300ms giữa mỗi ký tự) Chỉ 1 API call được thực hiện sau khi ngừng gõ 300ms, không phải 3 calls cho "p", "ph", "pho" P2PASS
TC-REST-013 Empty state — no restaurants Set location tới địa điểm không có nhà hàng (mock GPS) Empty state illustration + text "Chưa có nhà hàng tại khu vực của bạn" + button "Thay đổi địa chỉ" P3PASS

Cart & Checkout — TC-CART / TC-ORDER

TC IDTest CaseStepsExpectedPriorityResult
TC-CART-001 Thêm món từ nhà hàng khác → alert Thêm "Phở bò" từ Nhà Hàng A. Sau đó mở Nhà Hàng B → thêm "Cơm tấm" Alert "Giỏ hàng của bạn đang có món từ Nhà Hàng A. Bắt đầu đơn mới từ Nhà Hàng B?" với [Hủy] [Đồng ý] P1PASS
TC-CART-002 Cart persist sau khi kill app Thêm 2 món vào giỏ → force-kill app → mở lại app Giỏ hàng vẫn còn đủ 2 món, số lượng đúng P1PASS
TC-ORDER-001 Checkout — complete happy path (Stripe) Cart có 2 items → Checkout → chọn địa chỉ → chọn Visa → dùng Stripe test card 4242 4242 4242 4242 → Place Order Order Confirmation screen với order #QB-XXXXX. Cart cleared. Push notification trong 30 giây. P1PASS
TC-ORDER-012 Payment failed → retry flow Dùng Stripe test card 4000 0000 0000 0002 (declined) → Place Order Error "Thẻ bị từ chối. Vui lòng thử lại hoặc chọn phương thức khác." Order không được tạo. User có thể chọn lại payment và retry. P1PASS
TC-ORDER-015 Idempotency — double tap Place Order Tap "Place Order" 2 lần rất nhanh (< 300ms) Chỉ 1 order được tạo trong DB. Idempotency key trên API prevent duplicate. Button bị disabled sau tap đầu. P1PASS
🔌 API Test Cases — Postman / Newman

Base URL: https://staging.quickbite.app/api/v1. Auth header: Authorization: Bearer {token}. Test runner: Newman trong CI.

POST /auth/register

TC IDScenarioRequest BodyExpected ResponseResult
API-AUTH-001 Register thành công {"name":"Test User","email":"new@test.com","password":"Test123!"} 201 Created. Body: {"message":"OTP sent","userId":"uuid"}. OTP record tồn tại trong DB. PASS
API-AUTH-002 Duplicate email Email đã tồn tại trong DB 409 Conflict. {"error":"EMAIL_EXISTS","message":"..."} PASS
API-AUTH-003 Password quá yếu {"password":"123456"} 400 Bad Request. {"error":"VALIDATION_ERROR","fields":{"password":"..."}} PASS
API-AUTH-004 Missing required field Bỏ trống "name" 400 Bad Request. Error message rõ ràng, không expose stack trace PASS
API-AUTH-005 SQL Injection trong email field {"email":"test@test.com' OR '1'='1"} 400 Bad Request (validation fail). KHÔNG trả về 200 hoặc DB error. PASS

POST /auth/login

TC IDScenarioInputExpectedResult
API-AUTH-010 Login thành công Valid credentials 200 OK. Body: {"accessToken":"...","refreshToken":"...","expiresIn":900}. JWT valid, payload chứa userId và role. PASS
API-AUTH-011 Sai password Email đúng, password sai 401 Unauthorized. {"error":"INVALID_CREDENTIALS"}. Response time KHÔNG phải 200ms chẵn (timing attack mitigation — phải có constant-time comparison). PASS
API-AUTH-012 Account locked Login sau khi bị lock 429 Too Many Requests. Body có "retryAfter": 900 (seconds). PASS

GET /restaurants

TC IDScenarioQuery ParamsExpectedResult
API-REST-001 Lấy restaurants trong radius ?lat=10.762&lon=106.660&radius=5 200 OK. Array of restaurants, mỗi item có: id, name, imageUrl, rating, deliveryTime, deliveryFee, isOpen, distance (km). PASS
API-REST-002 No auth header Gọi API không có Bearer token 401 Unauthorized. {"error":"UNAUTHORIZED"} PASS
API-REST-003 Expired JWT token Dùng token đã quá 15 phút 401. {"error":"TOKEN_EXPIRED"}. Client phải dùng refresh token để lấy token mới. PASS
API-REST-004 Cache hit verification Gọi cùng endpoint 2 lần trong 5 phút Response header: X-Cache: HIT ở lần 2. Response time lần 2 < 50ms (vs lần 1 < 300ms). PASS

POST /orders — Checkout API

// Postman Test Script — TC-API-ORDER-001 const body = pm.response.json(); pm.test("Status 201 Created", () => pm.response.to.have.status(201)); pm.test("Response time < 2000ms", () => pm.expect(pm.response.responseTime).to.be.below(2000)); pm.test("Order ID format QB-XXXXXX", () => { pm.expect(body.orderId).to.match(/^QB-\d{6}$/); }); pm.test("Estimated delivery time present", () => { pm.expect(body).to.have.property("estimatedDeliveryMinutes"); pm.expect(body.estimatedDeliveryMinutes).to.be.above(0).and.below(120); }); pm.test("Cart cleared after order", () => { pm.expect(body.cartCleared).to.be.true; }); // Save orderId for downstream tests pm.environment.set("lastOrderId", body.orderId);
💡 Postman Collection Structure (Best Practice)
Folder: Auth → Restaurants → Menu → Cart → Orders → Payment. Mỗi folder có Pre-request Script setup (login, lấy token). Dùng Environment Variables: {{base_url}}, {{auth_token}}, {{test_user_email}}. Chạy trong CI: newman run QuickBite.postman_collection.json -e staging.json --reporter-htmlextra.
🗄️ Database Testing

Connect tới staging PostgreSQL. Verify data integrity sau các business operations.

-- TC-DB-001: Verify user created đúng sau registration SELECT u.id, u.email, u.name, u.is_verified, u.created_at, u.password_hash FROM users u WHERE u.email = 'new@test.com'; -- Expected: -- is_verified = false (chưa confirm OTP) -- password_hash NOT NULL và có prefix '$2b$' (bcrypt) -- password_hash KHÔNG PHẢI plaintext 'Test123!' -- created_at trong vòng 60 giây -- TC-DB-002: Verify OTP record created và có expiry SELECT otp_code, expires_at, is_used FROM otp_verifications WHERE user_id = '<userId>' ORDER BY created_at DESC LIMIT 1; -- Expected: expires_at = created_at + 10 minutes, is_used = false -- TC-DB-003: Verify ORDER transaction integrity (ACID) -- Sau khi order thành công: BEGIN; SELECT * FROM orders WHERE order_id = 'QB-123456'; -- status = 'confirmed', total_amount đúng SELECT * FROM order_items WHERE order_id = 'QB-123456'; -- Đủ items, quantity đúng, price snapshot đúng tại thời điểm order SELECT * FROM payments WHERE order_id = 'QB-123456'; -- status = 'succeeded', stripe_payment_intent_id NOT NULL ROLLBACK; -- TC-DB-004: Race condition test — 2 users order món cuối cùng -- Setup: Set menu item quantity = 1 UPDATE menu_items SET stock_quantity = 1 WHERE id = '<itemId>'; -- Sau khi 2 concurrent order requests: -- Expected: Chỉ 1 order thành công, 1 order nhận 409 "Hết hàng" -- stock_quantity = 0 (không phải -1) → check pessimistic locking -- TC-DB-005: Verify no PII in logs SELECT message FROM audit_logs WHERE message ILIKE '%password%' OR message ILIKE '%card_number%' OR message ILIKE '%cvv%'; -- Expected: 0 rows — no sensitive data in logs
⚠️ TC-DB-004 — Race Condition Bug Found
Khi chạy race condition test với 2 concurrent requests (dùng Postman's parallel runner), cả 2 orders được tạo thành công với stock_quantity = -1. Nguyên nhân: code dùng SELECT trước rồi UPDATE riêng, không có row-level lock. Fix cần thiết: Dùng UPDATE menu_items SET stock_quantity = stock_quantity - 1 WHERE id = ? AND stock_quantity > 0 RETURNING id trong 1 atomic statement. → Logged as BUG-004 (Critical).
📲 Mobile-Specific Testing
TC IDScenarioDeviceStepsExpectedResult
TC-MOB-001 Interruption — phone call trong lúc checkout Android 13 Ở Checkout screen → trigger incoming call (simulated) → answer → hang up → return to app App resume đúng Checkout screen, cart data còn nguyên, không tự-submit PASS
TC-MOB-002 Background → foreground sau 30 phút iOS 17 Mở app → background 30 phút → foreground JWT có thể expire (15 min). App tự silent refresh token. Không bị logout đột ngột. Nếu refresh token cũng expire → navigate Login với message. PASS
TC-MOB-003 Mất mạng giữa chừng khi checkout iPhone 14 + Android Bắt đầu checkout → tắt WiFi khi đang submit order App hiện "Mất kết nối. Vui lòng kiểm tra mạng." Không tạo partial order. Khi có mạng lại → user có thể retry. Không tạo duplicate order. PASS
TC-MOB-004 Xoay màn hình (orientation change) iPad (nếu supported) Ở Restaurant List → xoay landscape Layout adjust properly, grid có thể chuyển từ 2 columns sang 3. Data không mất. SKIP (không support iPad trong sprint này)
TC-MOB-005 Android Back Button — Checkout screen Android 13, 14 Ở Checkout → nhấn hardware Back button Alert "Bạn có muốn hủy đơn hàng không?" [Ở lại] [Hủy đơn]. Không tự navigate ra ngoài. PASS
TC-MOB-006 Deep link — share restaurant link iOS + Android Mở URL quickbite://restaurant/123 khi app đã install App mở và navigate thẳng đến Restaurant Detail screen của restaurant #123. Nếu chưa login → Login screen, sau login → navigate đúng restaurant. PASS
TC-MOB-007 Permission denied — Location iOS 16 + Android 13 Deny location permission → vào Home screen Hiện prompt "Cho phép vị trí để xem nhà hàng gần bạn" hoặc input field nhập địa chỉ tay. Không crash. PASS
TC-MOB-008 App update — data migration Android 13 Install v0.9 → add items to cart → upgrade to v1.0 → open app Cart data vẫn còn (backward compatible AsyncStorage schema). User không bị logout. PASS
📱 Device Test Matrix — Sprint 3
P1 Devices (phải test trên thiết bị thật): iPhone 14 (iOS 16), iPhone 15 Pro (iOS 17), Samsung Galaxy S24 (Android 14), Google Pixel 7 (Android 13).
P2 Devices (Simulator/Emulator OK): iPhone 12 Mini (iOS 16), OnePlus 11 (Android 13).
Excluded this sprint: Android < 11, iOS < 16, tablets.
Performance Testing — k6

Mục tiêu: Staging chịu tải 200 concurrent users (= 20% dự kiến peak production launch). SLA: P90 response < 2s, error rate < 1%.

// k6 load test — tests/performance/sprint3-load.js import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate, Trend } from 'k6/metrics'; const errorRate = new Rate('errors'); const restaurantLoadTime = new Trend('restaurant_load_time'); export const options = { stages: [ { duration: '1m', target: 50 }, // Ramp-up { duration: '3m', target: 200 }, // Peak load { duration: '1m', target: 200 }, // Sustain { duration: '1m', target: 0 }, // Ramp-down ], thresholds: { 'http_req_duration{endpoint:restaurants}': ['p(90)<2000'], 'http_req_duration{endpoint:menu}': ['p(90)<1500'], 'http_req_duration{endpoint:checkout}': ['p(90)<3000'], 'http_req_failed': ['rate<0.01'], // < 1% errors 'errors': ['rate<0.01'], }, }; const BASE_URL = 'https://staging.quickbite.app/api/v1'; export function setup() { // Login and return shared token for all VUs const res = http.post(`${BASE_URL}/auth/login`, JSON.stringify({ email: 'perf_test@quickbite.app', password: 'PerfTest123!' }), { headers: { 'Content-Type': 'application/json' } }); return { token: res.json('accessToken') }; } export default function(data) { const headers = { 'Authorization': `Bearer ${data.token}`, 'Content-Type': 'application/json', }; // Scenario 1: Browse restaurants (most common action) const restRes = http.get( `${BASE_URL}/restaurants?lat=10.762&lon=106.660&radius=5`, { headers, tags: { endpoint: 'restaurants' } } ); check(restRes, { 'restaurants status 200': r => r.status === 200, 'has restaurants array': r => r.json('restaurants').length > 0, }) || errorRate.add(1); restaurantLoadTime.add(restRes.timings.duration); sleep(2); // Scenario 2: View menu const menuRes = http.get( `${BASE_URL}/restaurants/1/menu`, { headers, tags: { endpoint: 'menu' } } ); check(menuRes, { 'menu status 200': r => r.status === 200 }) || errorRate.add(1); sleep(1); }

Performance Test Results — Sprint 3

EndpointP50P90P99Error RateTPSStatus
GET /restaurants180ms620ms1,200ms0.12%45✓ SLA Met
GET /restaurants/:id/menu95ms310ms750ms0.05%62✓ SLA Met
POST /auth/login210ms580ms1,100ms0.08%28✓ SLA Met
POST /orders890ms2,750ms4,200ms0.8%12✗ P90 FAIL (2750 > 2000ms)
GET /cart45ms120ms280ms0.01%110✓ SLA Met
⚠️ Performance Finding: POST /orders P90 = 2,750ms
SLA breach: P90 target 2,000ms, actual 2,750ms. Root cause analysis: N+1 query khi tạo order — code đang query SELECT * FROM menu_items WHERE id = ? trong loop cho mỗi cart item thay vì 1 query với IN (?). → BUG-005 (Major) filed. Fix: batch query menu_items với WHERE id IN (...) rồi map vào order items.
🔒 Security Testing — OWASP Top 10

Security testing chạy trên staging. Tools: OWASP ZAP (automated scan) + manual testing. Tập trung vào các điểm rủi ro cao của fintech/food delivery app.

TC IDOWASP CategoryTest ScenarioMethodExpectedResult
TC-SEC-001 A01: Broken Access Control User A xem order của User B bằng cách thay orderId trong URL Postman: GET /orders/QB-000001 với token của User B 403 Forbidden. Không trả về order data của User A cho User B. PASS
TC-SEC-002 A01: IDOR — Menu management Normal user gọi DELETE /admin/restaurants/1 Postman với user-level JWT 403 Forbidden. Role-based access control hoạt động. PASS
TC-SEC-003 A02: Cryptographic Failures Password stored dưới dạng gì trong DB? SQL: SELECT password_hash FROM users WHERE email='test@test.com' Giá trị bắt đầu bằng $2b$ (bcrypt, cost factor ≥ 10). KHÔNG phải MD5/SHA1/plaintext. PASS
TC-SEC-004 A03: Injection SQL injection trong search endpoint GET /restaurants?search=pho' OR '1'='1 Trả về kết quả bình thường (có filter) hoặc 400. KHÔNG trả về tất cả restaurants (injection failed). Dùng parameterized queries. PASS
TC-SEC-005 A03: NoSQL Injection Inject trong JSON body POST /auth/login body: {"email":{"$gt":""},"password":"x"} 400 Bad Request hoặc 401. KHÔNG bypass authentication. PASS
TC-SEC-006 A07: Auth Failures JWT algorithm confusion attack Modify JWT header "alg":"none", remove signature 401 Unauthorized. Server reject token với alg=none. PASS
TC-SEC-007 A07: Auth Failures Brute force OTP Script gửi 1000 OTP guesses liên tiếp Rate limit sau 10 attempts. 429 Too Many Requests với Retry-After header. IP blocked sau excessive attempts. FAIL — BUG-006
TC-SEC-008 A05: Security Misconfiguration Server response headers Xem response headers của bất kỳ API call Có: Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options: DENY. KHÔNG có: X-Powered-By: Express, Server: nginx/x.y.z. PASS
TC-SEC-009 A04: Insecure Design Payment amount tampering Postman: POST /orders với body tự set "totalAmount": 1 (thay vì giá thực) Server PHẢI tính lại total từ DB prices, không dùng client-submitted amount. Order total = giá thực từ database. PASS
TC-SEC-010 A09: Logging Failures Sensitive data trong logs Query CloudWatch logs / staging logs sau login và order operations KHÔNG thấy: password, card number, CVV, full JWT token trong logs. OK để log: userId, email (truncated), request method, status code. PASS
TC-SEC-011 Mobile: Secure Storage JWT stored ở đâu trong app? Root Android device, kiểm tra SharedPreferences và file system JWT và refresh token stored trong Android Keystore (không phải SharedPreferences plaintext) / iOS Keychain. Không thể extract mà không có biometrics. PASS
TC-SEC-012 Mobile: Certificate Pinning MITM attack với proxy (Charles/Burp Suite) Setup SSL proxy → chạy app App từ chối kết nối khi certificate không match pinned cert. Network traffic không thể intercepted. PASS
💡 BUG-006: OTP Brute Force — Severity Critical
OTP là 6 chữ số = 1,000,000 combinations. Không có rate limit → attacker có thể brute force trong vài phút. Fix: rate limit 10 attempts/15 min per user ID + per IP. OTP invalidation sau mỗi failed attempt (count). Alert security team sau 5 failed attempts.
🤖 Test Automation Suite

Automation strategy: Detox cho E2E mobile, Newman cho API regression, k6 cho performance trong CI pipeline.

// Detox E2E — e2e/auth/registration.test.ts import { device, element, by, expect } from 'detox'; describe('User Registration', () => { beforeAll(async () => { await device.launchApp({ newInstance: true }); }); beforeEach(async () => { await device.reloadReactNative(); }); it('TC-AUTH-001: should register successfully and navigate to OTP screen', async () => { // Navigate to Register screen await element(by.id('register-link')).tap(); // Fill in registration form await element(by.id('name-input')).typeText('Test User'); await element(by.id('email-input')).typeText('newuser@test.com'); await element(by.id('password-input')).typeText('Test123!'); // Submit await element(by.id('register-button')).tap(); // Assert OTP screen appeared await expect(element(by.id('otp-screen'))).toBeVisible(); await expect(element(by.text('OTP đã gửi tới newuser@test.com'))).toBeVisible(); }); it('TC-AUTH-004: should show error for invalid email format', async () => { await element(by.id('register-link')).tap(); await element(by.id('email-input')).typeText('notanemail'); await element(by.id('name-input')).tap(); // trigger blur on email await expect(element(by.text('Email không hợp lệ'))).toBeVisible(); await expect(element(by.id('register-button'))).not.toBeEnabled(); }); it('TC-AUTH-005: should show duplicate email error', async () => { await element(by.id('register-link')).tap(); await element(by.id('name-input')).typeText('Existing User'); await element(by.id('email-input')).typeText('existing@test.com'); // pre-seeded await element(by.id('password-input')).typeText('Test123!'); await element(by.id('register-button')).tap(); await expect(element(by.text('Email này đã được đăng ký. Đăng nhập?'))).toBeVisible(); }); }); describe('Order Checkout', () => { beforeAll(async () => { await device.launchApp({ newInstance: true }); await loginAsTestUser(); // helper function await addItemToCart('Phở bò đặc biệt', 1); // helper function }); it('TC-ORDER-015: should prevent double order on double tap', async () => { await element(by.id('cart-button')).tap(); await element(by.id('checkout-button')).tap(); // Select address and payment await element(by.id('address-1')).tap(); await element(by.id('pay-visa')).tap(); await fillStripeTestCard(); // helper // Double tap place order very fast await element(by.id('place-order-btn')).multiTap(2); // Should see only ONE confirmation await expect(element(by.id('order-confirmation-screen'))).toBeVisible(); // Verify in API only 1 order created const orderId = await element(by.id('order-id')).getAttributes(); expect(orderId).toMatch(/^QB-\d{6}$/); }); });
📌 GitHub Actions CI — Automation Pipeline
# .github/workflows/qa-pipeline.yml on: pull_request: { branches: [main, develop] } push: { branches: [develop] } jobs: api-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Newman API tests run: newman run tests/api/QuickBite.postman_collection.json -e staging.json --reporter-htmlextra - uses: actions/upload-artifact@v4 if: always() with: { name: api-test-report, path: newman/ } e2e-ios: runs-on: macos-latest steps: - uses: actions/checkout@v4 - run: brew tap wix/brew && brew install applesimutils - run: npm ci - run: npx detox build -c ios.sim.release - run: npx detox test -c ios.sim.release --record-videos all - uses: actions/upload-artifact@v4 if: failure() with: { name: detox-ios-artifacts, path: artifacts/ } e2e-android: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: reactivecircus/android-emulator-runner@v2 with: api-level: 33 script: npx detox test -c android.emu.release --headless
🐛 Bug Reports — Sprint 3

6 bugs tìm được. 2 Critical, 2 Major, 2 Minor. Format theo ISTQB standard.

BUG-001 Password strength indicator không cập nhật real-time khi gõ ký tự đầu tiên Minor
Component
RegisterScreen — PasswordStrengthIndicator component
Priority
P3 — Low (UX issue, không block functionality)
Steps to Reproduce
1. Mở Register screen
2. Tap vào Password field
3. Gõ ký tự đầu tiên "T"
4. Quan sát strength indicator
Expected Behavior
Strength indicator hiện "Weak" ngay sau khi gõ ký tự đầu tiên
Actual Behavior
Strength indicator vẫn trống (không hiện) sau ký tự đầu. Chỉ hiện từ ký tự thứ 2 trở đi.
Environment
iOS 17 (iPhone 15 Pro), App v1.0.0 Build 47
Root Cause (Dev)
Off-by-one error trong useEffect dependency — trigger khi password.length > 1 thay vì >= 1
Linked AC
US-001 AC-005 | TC: TC-AUTH-010
BUG-002 Google OAuth + Email account merge tạo duplicate user thay vì merge Major
Component
Auth Service — OAuth Controller
Priority
P1 — High (data integrity issue)
Steps to Reproduce
1. Đăng ký account với email abc@gmail.com + password
2. Logout
3. Tap "Continue with Google" → dùng cùng abc@gmail.com account
4. Check DB
Expected
1 user record trong DB với cả password auth VÀ google_id linked. User giữ lịch sử orders.
Actual
2 user records với cùng email. User bị login vào account mới rỗng, mất toàn bộ order history.
Severity Justification
Major: Dữ liệu bị duplicate, user mất order history. Không crash app nhưng data integrity bị ảnh hưởng nghiêm trọng.
Status
In Progress (Dev assigned: Minh Đức)
BUG-003 Pull-to-refresh không bypass Redis cache — vẫn trả dữ liệu cũ Major
Component
Restaurant API — GET /restaurants handler + Redis cache layer
Priority
P1 — High (user thấy stale data, có thể order nhà hàng đã đóng)
Steps to Reproduce
1. Mở Home screen → restaurants loaded (cache populated)
2. Admin đổi 1 nhà hàng từ "Mở" → "Đóng" trong dashboard
3. Trong vòng 5 phút: Pull-to-refresh trên Home screen
4. Kiểm tra nhà hàng đó có badge "Đóng" không?
Actual
Nhà hàng vẫn hiển thị "Mở" dù đã đổi. Pull-to-refresh không invalidate cache — vẫn serve cached response từ Redis. Chỉ cập nhật sau 5 phút khi cache expire tự nhiên.
Impact
User tap vào nhà hàng "Mở" (stale) → thêm vào cart → checkout → order fail "Nhà hàng đã đóng cửa." Poor UX + potential lost order revenue.
Fix Suggestion
Thêm Cache-Control: no-cache request header khi app gửi pull-to-refresh. API handler check header này → skip Redis, fetch từ DB → update cache.
Status
Open — Fix Planned Sprint 4
BUG-004 [CRITICAL] Race condition: 2 users có thể order cùng 1 món cuối cùng, stock_quantity = -1 Critical
Component
Order Service — createOrder() function, menu_items table
Priority
P1 — BLOCKER (data corruption, financial impact)
Steps to Reproduce
1. Set menu item "Phở bò đặc biệt" stock = 1 trong DB
2. Dùng 2 Postman tabs: cả 2 send POST /orders với item trên, gửi đồng thời
3. Check DB: SELECT stock_quantity FROM menu_items WHERE id = '...'
Actual
stock_quantity = -1. Cả 2 orders có status "confirmed". Restaurant nhận 2 orders nhưng chỉ còn 1 món. Có thể gây customer complaint và refund.
Root Cause
Code hiện tại: SELECT stock_quantity WHERE id = ? → if > 0 → UPDATE SET stock_quantity - 1. Đây là TOCTOU (Time-of-Check-Time-of-Use) race condition. Giữa SELECT và UPDATE, 2 transactions cùng thấy stock = 1.
Fix (Required before release)
Atomic UPDATE: UPDATE menu_items SET stock_quantity = stock_quantity - 1 WHERE id = ? AND stock_quantity > 0 RETURNING id. Nếu RETURNING trả về empty → throw "Out of stock" error. Toàn bộ trong 1 DB transaction.
Status
🔴 BLOCKER — Must Fix Before Release
BUG-006 [CRITICAL] OTP endpoint không có rate limiting — dễ bị brute force Critical
Component
Auth Service — POST /auth/verify-otp
Priority
P1 — BLOCKER (Security vulnerability)
Proof of Concept
Script Python gửi 10,000 POST requests với OTP 000000 đến 009999 trong 47 giây. Không bị block. Với OTP 6 chữ số: max 1,000,000 combinations = có thể crack trong ~78 phút.
Impact
Attacker có thể takeover bất kỳ account nào đang trong trạng thái "chờ OTP verify". Đặc biệt nguy hiểm vì app có payment data.
Fix Required
1. Max 5 OTP attempts per userId, sau đó expire OTP và yêu cầu resend. 2. Rate limit IP: 10 req/min. 3. OTP expiry sau mỗi failed attempt.
Status
🔴 BLOCKER — Security Team Notified
📊 Test Metrics — Sprint 3 Summary
42
Total Test Cases
38
Executed
35
Passed
3
Failed
4
Blocked/Skipped
92%
Pass Rate
90%
Req Coverage
6
Bugs Found
KPITargetActualStatus
Test Case Execution Rate≥90%90.5% (38/42)✓ Met
Pass Rate≥88%92.1% (35/38)✓ Met
Critical Bugs Open02 (BUG-004, BUG-006)✗ Not Met
Major Bugs Open≤12 (BUG-002, BUG-003)✗ Not Met
API Performance P90<2s cho tất cả endpointsPOST /orders P90 = 2,750ms✗ Not Met
Security Scan0 Critical/High vulns1 Critical (BUG-006 OTP)✗ Not Met
Defect Leakage (từ Sprint 2)<5%0% (không tìm được bug cũ)✓ Met
Automation Coverage≥60% P1 test cases72% (18/25 P1 cases automated)✓ Met

Bug Distribution

SeverityCountFixedOpenDefer
Critical2020
Major2011 (Sprint 4)
Minor1100
Trivial1001
Total6132
🚦 Go / No-Go Release Report — Sprint 3
🏆 Senior QA — Release Decision Framework
Go/No-Go không phải là quyết định của QA một mình. QA cung cấp dữ liệu khách quan, PM/Tech Lead ra quyết định cuối cùng với risk acceptance. QA có thể và nên nói "No-Go với lý do X" — nhưng luôn kèm theo: "Đây là những gì cần fix để đạt Go."
CriteriaRequirementActualDecision
Test Case Pass Rate≥88%92.1%✅ Go
Critical Bugs0 open2 open (BUG-004 race condition, BUG-006 security)🔴 No-Go
Security Vulnerabilities0 Critical/High1 Critical (OTP brute force)🔴 No-Go
API PerformanceP90 <2s all endpointsPOST /orders P90 2,750ms🟡 Conditional
Requirement Coverage100% US tested90% (iPad scenarios skipped)🟡 Conditional
Regression0 regressions from Sprint 20 regressions found✅ Go
Google OAuth mergeNo duplicate usersDuplicate users created (BUG-002)🔴 No-Go
📋 QA Official Recommendation — Sprint 3

Recommendation: NO-GO

Blockers (phải fix trước khi release):

  1. BUG-004 — Race condition: stock_quantity có thể âm → overselling. Critical data integrity issue với financial impact trực tiếp.
  2. BUG-006 — OTP brute force: Security vulnerability cho phép account takeover. Với payment data trong app, đây là unacceptable risk.
  3. BUG-002 — Google OAuth merge: User mất order history. Data integrity + poor UX.

Có thể release với risk acceptance (PM sign-off required):

  1. BUG-005 — Performance /orders P90 2,750ms: Có thể defer nếu không phải peak season và monitor closely post-launch.
  2. BUG-003 — Pull-to-refresh cache: Workaround là đợi 5 phút. Defer to Sprint 4.

Estimated fix time: 2-3 ngày cho 3 blockers. Recommend mini-regression (1 ngày) sau khi fix. Revised release date: +4 days.

Signed: QA Engineer — Sprint 3 | Date: 2026-06-04

💡 Lessons Learned — Sprint 3
1. 3 Amigos sớm hơn: BUG-002 (OAuth merge) và BUG-003 (cache strategy) có thể được phát hiện trong Requirements Review nếu QA tham gia 3 Amigos meeting từ đầu sprint.
2. Security testing phải là mandatory, không phải optional: BUG-006 là critical security issue — nên chạy basic OWASP check trong CI, không chờ đến cuối sprint.
3. Performance regression test cần baseline: Chưa có Sprint 2 baseline nên khó biết P90 2,750ms là regression hay original state.
4. Race condition là blind spot: Cần thêm concurrent testing vào test checklist cho tất cả order/inventory operations.