Testing Strategy - Full Flight Simulation
Philosophy: Every pull request runs a complete "simulated flight" - testing the entire passenger journey from search to post-flight, including edge cases, failures, and recovery scenarios. Inspired by SpaceX's approach of simulating a full rocket mission before every launch.
The SpaceX Principle
┌─────────────────────────────────────────────────────────────────┐
│ │
│ "If you can't test it end-to-end, you can't ship it" │
│ │
│ Every PR triggers: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ FULL PASSENGER JOURNEY SIMULATION │ │
│ │ │ │
│ │ Search → Book → Pay → Check-in → Board → Fly → Land │ │
│ │ ↓ │ │
│ │ + 50+ edge cases │ │
│ │ + Failure scenarios │ │
│ │ + Recovery paths │ │
│ │ + Concurrent operations │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ If ANY scenario fails → PR blocked │
│ │
└─────────────────────────────────────────────────────────────────┘
Testing Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Testing Pyramid │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ E2E │ Full journey simulations │
│ │ Tests │ (~20 min) │
│ ┌┴───────┴┐ │
│ │Integration│ API + DB + Services │
│ │ Tests │ (~5 min) │
│ ┌┴──────────┴┐ │
│ │ Unit │ Business logic │
│ │ Tests │ (~30 sec) │
│ ┌┴────────────┴┐ │
│ │ Static │ Types, linting │
│ │ Analysis │ (~10 sec) │
│ └──────────────┘ │
│ │
│ Total PR pipeline: ~30 minutes │
│ All layers run in parallel where possible │
│ │
└─────────────────────────────────────────────────────────────────┘
Full Journey Test Suites
Suite 1: Happy Path - Complete Passenger Journey
describe('Full Passenger Journey - Happy Path', () => {
it('completes booking → check-in → boarding → flight', async () => {
// 1. SEARCH
const flights = await searchFlights({
origin: 'CCS',
destination: 'MIA',
date: '2024-06-15',
passengers: 2
})
expect(flights.length).toBeGreaterThan(0)
// 2. SELECT FLIGHT
const selectedFlight = flights[0]
const availability = await getAvailability(selectedFlight.id)
expect(availability.economy.available).toBeGreaterThanOrEqual(2)
// 3. CREATE BOOKING
const booking = await createBooking({
flightId: selectedFlight.id,
passengers: [
{ firstName: 'John', lastName: 'Doe', type: 'ADULT' },
{ firstName: 'Jane', lastName: 'Doe', type: 'ADULT' }
],
contact: { email: 'john@test.com', phone: '+1234567890' }
})
expect(booking.pnr).toMatch(/^[A-Z0-9]{6}$/)
expect(booking.status).toBe('PENDING')
// 4. ADD ANCILLARIES
const seatSelection = await selectSeats(booking.id, [
{ passengerId: booking.passengers[0].id, seat: '12A' },
{ passengerId: booking.passengers[1].id, seat: '12B' }
])
expect(seatSelection.success).toBe(true)
// 5. PAYMENT
const payment = await processPayment(booking.id, {
method: 'CREDIT_CARD',
token: 'tok_visa_test',
amount: booking.totalAmount
})
expect(payment.status).toBe('COMPLETED')
// 6. TICKETING
const tickets = await issueTickets(booking.id)
expect(tickets.length).toBe(2)
expect(tickets[0].status).toBe('OPEN')
// 7. RETRIEVE BOOKING (verify persistence)
const retrievedBooking = await getBooking(booking.pnr)
expect(retrievedBooking.status).toBe('TICKETED')
// 8. WEB CHECK-IN (24h before departure)
await timeTravelTo(hoursBeforeFlight(24))
const checkIn = await performCheckIn(booking.pnr, {
passengers: booking.passengers.map(p => p.id)
})
expect(checkIn.boardingPasses.length).toBe(2)
// 9. BOARDING PASS GENERATION
const boardingPass = checkIn.boardingPasses[0]
expect(boardingPass.bcbp).toBeDefined() // IATA BCBP format
expect(boardingPass.qrCode).toBeDefined()
// 10. AIRPORT CHECK-IN VERIFICATION
const verification = await verifyBoardingPass(boardingPass.bcbp)
expect(verification.valid).toBe(true)
// 11. BOARDING
await timeTravelTo(minutesBeforeFlight(45))
const boarding = await boardPassenger(
selectedFlight.id,
booking.passengers[0].id
)
expect(boarding.status).toBe('BOARDED')
// 12. FLIGHT DEPARTURE
await timeTravelTo(scheduledDeparture(selectedFlight))
await departFlight(selectedFlight.id)
const flightStatus = await getFlightStatus(selectedFlight.id)
expect(flightStatus).toBe('DEPARTED')
// 13. FLIGHT ARRIVAL
await timeTravelTo(scheduledArrival(selectedFlight))
await arriveFlight(selectedFlight.id)
// 14. POST-FLIGHT VERIFICATION
const finalBooking = await getBooking(booking.pnr)
expect(finalBooking.status).toBe('FLOWN')
expect(finalBooking.segments[0].status).toBe('FLOWN')
// 15. LOYALTY CREDIT
const loyaltyAccount = await getLoyaltyAccount('john@test.com')
expect(loyaltyAccount.currentSpend).toBeGreaterThan(0)
})
})
Suite 2: Booking Edge Cases
describe('Booking Edge Cases', () => {
describe('Inventory Management', () => {
it('prevents overbooking when last seat is taken', async () => {
// Setup: Flight with 1 seat remaining
const flight = await createFlightWithInventory({ economy: 1 })
// User A starts booking
const bookingA = await startBooking(flight.id, 1)
// User B tries to book same seat concurrently
const bookingB = startBooking(flight.id, 1) // Don't await
// User A completes first
await completeBooking(bookingA.id)
// User B should fail
await expect(bookingB).rejects.toThrow('Insufficient inventory')
})
it('releases held inventory after timeout', async () => {
const flight = await createFlightWithInventory({ economy: 1 })
// Start booking but don't complete
const booking = await startBooking(flight.id, 1)
expect(await getAvailability(flight.id)).toBe(0)
// Wait for hold timeout (15 min)
await timeTravelForward(minutes(16))
// Inventory should be released
expect(await getAvailability(flight.id)).toBe(1)
expect(await getBookingStatus(booking.id)).toBe('EXPIRED')
})
it('handles concurrent seat selection', async () => {
const flight = await createFlightWithInventory({ economy: 10 })
const booking1 = await createBooking(flight.id, 1)
const booking2 = await createBooking(flight.id, 1)
// Both try to select seat 12A simultaneously
const [result1, result2] = await Promise.allSettled([
selectSeat(booking1.id, '12A'),
selectSeat(booking2.id, '12A')
])
// One succeeds, one fails
const successes = [result1, result2].filter(r => r.status === 'fulfilled')
const failures = [result1, result2].filter(r => r.status === 'rejected')
expect(successes.length).toBe(1)
expect(failures.length).toBe(1)
})
})
describe('Payment Scenarios', () => {
it('handles payment failure and allows retry', async () => {
const booking = await createBookingWithPaymentPending()
// First payment fails
const failedPayment = await processPayment(booking.id, {
token: 'tok_card_declined'
})
expect(failedPayment.status).toBe('FAILED')
expect(await getBookingStatus(booking.id)).toBe('PENDING')
// Retry with valid card
const successPayment = await processPayment(booking.id, {
token: 'tok_visa_test'
})
expect(successPayment.status).toBe('COMPLETED')
})
it('prevents double payment', async () => {
const booking = await createBookingWithPaymentPending()
// Process payment twice concurrently
const [pay1, pay2] = await Promise.allSettled([
processPayment(booking.id, { token: 'tok_visa_test' }),
processPayment(booking.id, { token: 'tok_visa_test' })
])
// Only one should succeed
const successes = [pay1, pay2].filter(r =>
r.status === 'fulfilled' && r.value.status === 'COMPLETED'
)
expect(successes.length).toBe(1)
})
it('handles partial refund correctly', async () => {
const booking = await createTicketedBooking({ amount: 500 })
// Cancel one passenger
await cancelPassenger(booking.id, booking.passengers[0].id)
// Verify partial refund
const refund = await getRefunds(booking.id)
expect(refund.amount).toBeLessThan(500)
expect(refund.status).toBe('PROCESSED')
})
})
describe('PNR Management', () => {
it('generates unique PNRs under high load', async () => {
const pnrs = await Promise.all(
Array(100).fill(null).map(() => createBooking())
)
const uniquePnrs = new Set(pnrs.map(b => b.pnr))
expect(uniquePnrs.size).toBe(100)
})
it('splits PNR correctly when passengers diverge', async () => {
// Booking with 2 passengers
const booking = await createBookingWithPassengers(2)
// Change flight for one passenger only
const newFlight = await getAlternativeFlight(booking.flightId)
const splitResult = await splitBooking(booking.id, {
passengerId: booking.passengers[0].id,
newFlightId: newFlight.id
})
expect(splitResult.originalPnr).toBe(booking.pnr)
expect(splitResult.newPnr).not.toBe(booking.pnr)
expect(splitResult.newPnr).toMatch(/^[A-Z0-9]{6}$/)
})
})
})
Suite 3: Check-in & Boarding Edge Cases
describe('Check-in & Boarding Edge Cases', () => {
describe('Web Check-in', () => {
it('blocks check-in before 24h window', async () => {
const booking = await createTicketedBooking({
departureIn: hours(48)
})
await expect(performCheckIn(booking.pnr))
.rejects.toThrow('Check-in not yet available')
})
it('blocks check-in after window closes', async () => {
const booking = await createTicketedBooking({
departureIn: minutes(30)
})
await expect(performCheckIn(booking.pnr))
.rejects.toThrow('Check-in closed')
})
it('handles check-in for infant with adult', async () => {
const booking = await createBookingWithInfant()
const checkIn = await performCheckIn(booking.pnr, {
passengers: [
booking.passengers.find(p => p.type === 'ADULT').id,
booking.passengers.find(p => p.type === 'INFANT').id
]
})
// Infant should be on adult's boarding pass
expect(checkIn.boardingPasses.length).toBe(1)
expect(checkIn.boardingPasses[0].infantIncluded).toBe(true)
})
it('requires document verification for international', async () => {
const booking = await createInternationalBooking()
// Check-in without documents should fail
await expect(performCheckIn(booking.pnr))
.rejects.toThrow('Travel documents required')
// Add passport
await addTravelDocument(booking.passengers[0].id, {
type: 'PASSPORT',
number: 'AB123456',
expiry: '2026-01-01',
nationality: 'US'
})
// Now check-in should work
const checkIn = await performCheckIn(booking.pnr)
expect(checkIn.success).toBe(true)
})
})
describe('Kiosk Check-in', () => {
it('handles kiosk check-in with PNR lookup', async () => {
const booking = await createTicketedBooking()
const kioskSession = await startKioskSession()
const lookup = await kioskLookup(kioskSession.id, {
pnr: booking.pnr,
lastName: booking.passengers[0].lastName
})
expect(lookup.booking.pnr).toBe(booking.pnr)
const checkIn = await kioskCheckIn(kioskSession.id, {
passengers: [booking.passengers[0].id]
})
expect(checkIn.boardingPasses.length).toBe(1)
expect(checkIn.printJob.status).toBe('QUEUED')
})
it('handles bag drop at kiosk', async () => {
const booking = await createCheckedInBooking()
const kioskSession = await startKioskSession()
const bagDrop = await kioskBagDrop(kioskSession.id, {
bookingRef: booking.pnr,
bags: [{ weight: 20 }]
})
expect(bagDrop.bagTags.length).toBe(1)
expect(bagDrop.bagTags[0]).toMatch(/^[0-9]{10}$/)
})
it('handles kiosk timeout and session recovery', async () => {
const booking = await createTicketedBooking()
// Start session
const session1 = await startKioskSession()
await kioskLookup(session1.id, { pnr: booking.pnr })
// Session times out
await timeTravelForward(minutes(3))
// New session should start fresh
const session2 = await startKioskSession()
expect(session2.id).not.toBe(session1.id)
})
})
describe('Boarding', () => {
it('enforces boarding sequence (zones)', async () => {
const flight = await createFlightWithBoardingZones()
const bookings = await createMultipleCheckedInBookings(flight.id, 10)
// Try to board zone 3 passenger before zone 1
const zone3Passenger = bookings.find(b =>
b.boardingPass.zone === 3
).passengers[0]
await expect(boardPassenger(flight.id, zone3Passenger.id))
.rejects.toThrow('Zone 3 not yet boarding')
// Board zone 1 first
await startBoardingZone(flight.id, 1)
const zone1Passenger = bookings.find(b =>
b.boardingPass.zone === 1
).passengers[0]
const boarding = await boardPassenger(flight.id, zone1Passenger.id)
expect(boarding.status).toBe('BOARDED')
})
it('handles standby passengers', async () => {
const flight = await createFullFlight()
const standbyBooking = await createStandbyBooking(flight.id)
// Initially cannot board
await expect(boardPassenger(flight.id, standbyBooking.passengers[0].id))
.rejects.toThrow('Standby - not cleared')
// One passenger no-shows
await markNoShow(flight.id, someOtherPassenger.id)
// Clear standby
await clearStandby(flight.id, standbyBooking.id)
// Now can board
const boarding = await boardPassenger(
flight.id,
standbyBooking.passengers[0].id
)
expect(boarding.status).toBe('BOARDED')
})
it('detects duplicate boarding attempt', async () => {
const booking = await createCheckedInBooking()
// First boarding succeeds
await boardPassenger(booking.flightId, booking.passengers[0].id)
// Second attempt fails
await expect(boardPassenger(booking.flightId, booking.passengers[0].id))
.rejects.toThrow('Already boarded')
})
})
})
Suite 4: Disruption & Recovery
describe('Disruption & Recovery Scenarios', () => {
describe('Flight Cancellation', () => {
it('automatically rebooks passengers on next available', async () => {
const booking = await createTicketedBooking()
const originalFlight = booking.segments[0].flight
// Cancel flight
await cancelFlight(originalFlight.id, {
reason: 'MECHANICAL',
notifyPassengers: true
})
// Verify automatic rebooking
const updatedBooking = await getBooking(booking.pnr)
expect(updatedBooking.segments[0].flight.id).not.toBe(originalFlight.id)
expect(updatedBooking.status).toBe('REBOOKED')
// Verify notification sent
const notifications = await getNotifications(booking.contactEmail)
expect(notifications).toContainEqual(
expect.objectContaining({
type: 'FLIGHT_CANCELLED',
newFlight: expect.any(Object)
})
)
})
it('offers refund when no alternatives available', async () => {
const booking = await createTicketedBooking()
// Cancel with no alternatives
await cancelFlight(booking.segments[0].flight.id, {
reason: 'WEATHER',
noAlternatives: true
})
const updatedBooking = await getBooking(booking.pnr)
expect(updatedBooking.refundEligible).toBe(true)
// Process refund
const refund = await requestRefund(booking.pnr)
expect(refund.amount).toBe(booking.totalAmount)
})
})
describe('Flight Delay', () => {
it('updates departure time and notifies passengers', async () => {
const booking = await createCheckedInBooking()
// Delay flight by 2 hours
await delayFlight(booking.segments[0].flight.id, {
newDeparture: hoursLater(2),
reason: 'ATC_DELAY'
})
// Verify boarding pass updated
const updatedBoardingPass = await getBoardingPass(
booking.pnr,
booking.passengers[0].id
)
expect(updatedBoardingPass.departureTime).toBe(hoursLater(2))
// Verify notification
const notifications = await getNotifications(booking.contactEmail)
expect(notifications).toContainEqual(
expect.objectContaining({
type: 'FLIGHT_DELAYED',
delayMinutes: 120
})
)
})
it('handles missed connection due to delay', async () => {
const booking = await createConnectingFlightBooking()
const firstFlight = booking.segments[0].flight
const connectionFlight = booking.segments[1].flight
// Delay first flight causing missed connection
await delayFlight(firstFlight.id, {
newDeparture: hoursLater(3) // Now arrives after connection departs
})
// System should detect and rebook connection
const updatedBooking = await getBooking(booking.pnr)
expect(updatedBooking.segments[1].flight.id).not.toBe(connectionFlight.id)
expect(updatedBooking.segments[1].status).toBe('REBOOKED')
})
})
describe('Passenger Issues', () => {
it('handles no-show and releases seat', async () => {
const booking = await createCheckedInBooking()
// Flight departs without passenger
await timeTravelTo(afterDeparture(booking.segments[0].flight))
await processNoShows(booking.segments[0].flight.id)
const updatedBooking = await getBooking(booking.pnr)
expect(updatedBooking.segments[0].status).toBe('NO_SHOW')
// Ticket should be updated
expect(updatedBooking.tickets[0].status).toBe('VOID')
})
it('handles denied boarding with compensation', async () => {
const flight = await createOverSoldFlight()
const lastBooking = await getLastBookingForFlight(flight.id)
// Deny boarding due to oversell
const denial = await denyBoarding(flight.id, lastBooking.passengers[0].id, {
reason: 'OVERSELL',
voluntary: false
})
expect(denial.compensation.amount).toBeGreaterThan(0)
expect(denial.rebooking).toBeDefined()
// Verify in booking history
const history = await getBookingHistory(lastBooking.pnr)
expect(history).toContainEqual(
expect.objectContaining({
event: 'DENIED_BOARDING',
compensation: expect.any(Number)
})
)
})
})
describe('System Recovery', () => {
it('recovers from payment processor outage', async () => {
const booking = await createBookingWithPaymentPending()
// Simulate payment processor down
await simulateOutage('payment-processor')
// Payment should be queued
const paymentResult = await processPayment(booking.id, {
token: 'tok_visa_test'
})
expect(paymentResult.status).toBe('QUEUED')
// Restore service
await restoreService('payment-processor')
await processPaymentQueue()
// Payment should complete
const updatedBooking = await getBooking(booking.pnr)
expect(updatedBooking.status).toBe('TICKETED')
})
it('maintains consistency during database failover', async () => {
// Start a booking
const booking = await startBooking()
// Trigger DB failover mid-transaction
await triggerDatabaseFailover()
// Booking should either complete or cleanly fail
const result = await completeBooking(booking.id).catch(e => e)
if (result instanceof Error) {
// If failed, inventory should be released
const availability = await getAvailability(booking.flightId)
expect(availability).toBe(originalAvailability)
} else {
// If succeeded, booking should be valid
const retrievedBooking = await getBooking(result.pnr)
expect(retrievedBooking).toBeDefined()
}
})
})
})
Suite 5: Loyalty Program
describe('Loyalty Program - Spend Based', () => {
it('tracks spend and upgrades tier automatically', async () => {
// Create member
const member = await enrollLoyaltyMember({
email: 'frequent@test.com',
firstName: 'Frequent',
lastName: 'Flyer'
})
expect(member.tier).toBe('MEMBER')
expect(member.currentSpend).toBe(0)
// Book flights totaling $1,200
await createAndCompleteBooking({
memberEmail: 'frequent@test.com',
amount: 600
})
await createAndCompleteBooking({
memberEmail: 'frequent@test.com',
amount: 600
})
// Should be Silver now
const updatedMember = await getLoyaltyAccount('frequent@test.com')
expect(updatedMember.tier).toBe('SILVER')
expect(updatedMember.currentSpend).toBe(1200)
})
it('applies tier benefits during booking', async () => {
// Gold member gets free seat selection
const goldMember = await createGoldMember()
const booking = await createBooking({
memberEmail: goldMember.email,
flightId: testFlight.id
})
// Premium seat should be free
const seatSelection = await selectPremiumSeat(booking.id, '1A')
expect(seatSelection.fee).toBe(0)
})
it('handles rolling 12-month qualification window', async () => {
// Member with spend from 13 months ago
const member = await createMemberWithHistoricalSpend({
spendDate: monthsAgo(13),
amount: 2000
})
// Should have dropped spend
const account = await getLoyaltyAccount(member.email)
expect(account.currentSpend).toBe(0)
expect(account.tier).toBe('MEMBER') // Dropped from Silver
})
})
Suite 6: Load & Concurrency
describe('Load & Concurrency Tests', () => {
it('handles 100 concurrent bookings', async () => {
const flight = await createFlightWithInventory({ economy: 100 })
// 100 concurrent booking attempts
const results = await Promise.allSettled(
Array(100).fill(null).map(() =>
createBookingWithPayment(flight.id, 1)
)
)
const successes = results.filter(r => r.status === 'fulfilled')
const failures = results.filter(r => r.status === 'rejected')
expect(successes.length).toBe(100)
expect(failures.length).toBe(0)
// Verify inventory is accurate
const finalAvailability = await getAvailability(flight.id)
expect(finalAvailability).toBe(0)
})
it('handles flash sale scenario', async () => {
// 10 seats, 50 concurrent requests
const flight = await createFlightWithInventory({ economy: 10 })
const results = await Promise.allSettled(
Array(50).fill(null).map(() =>
createBookingWithPayment(flight.id, 1)
)
)
const successes = results.filter(r => r.status === 'fulfilled')
// Exactly 10 should succeed
expect(successes.length).toBe(10)
// Inventory should be exactly 0
expect(await getAvailability(flight.id)).toBe(0)
})
it('maintains response times under load', async () => {
const flight = await createFlightWithInventory({ economy: 1000 })
const startTime = Date.now()
// 50 concurrent searches
await Promise.all(
Array(50).fill(null).map(() =>
searchFlights({
origin: 'CCS',
destination: 'MIA',
date: '2024-06-15'
})
)
)
const duration = Date.now() - startTime
const avgLatency = duration / 50
expect(avgLatency).toBeLessThan(200) // <200ms average
})
})
Test Infrastructure
Test Database Strategy
// prisma/seed.test.ts
import { PrismaClient } from '@prisma/client'
import { execSync } from 'child_process'
const prisma = new PrismaClient()
export async function setupTestDatabase() {
// Create isolated test database
const testDbUrl = process.env.TEST_DATABASE_URL
// Run migrations
execSync(`DATABASE_URL=${testDbUrl} npx prisma migrate deploy`)
// Seed base data
await seedTestData(prisma)
}
export async function teardownTestDatabase() {
// Clean all data
await prisma.$executeRaw`TRUNCATE TABLE bookings CASCADE`
await prisma.$executeRaw`TRUNCATE TABLE flights CASCADE`
// ... etc
}
export async function resetBetweenTests() {
// Fast reset using transactions
await prisma.$executeRaw`BEGIN`
// Tests run here
await prisma.$executeRaw`ROLLBACK`
}
Time Travel Utilities
// test/utils/time-travel.ts
let mockNow: Date | null = null
export function timeTravelTo(date: Date) {
mockNow = date
jest.setSystemTime(date)
}
export function timeTravelForward(duration: number) {
const newTime = new Date((mockNow || new Date()).getTime() + duration)
timeTravelTo(newTime)
}
export function resetTime() {
mockNow = null
jest.useRealTimers()
}
// Helpers
export const hours = (n: number) => n * 60 * 60 * 1000
export const minutes = (n: number) => n * 60 * 1000
export const hoursBeforeFlight = (n: number, flight: Flight) =>
new Date(flight.scheduledDeparture.getTime() - hours(n))
Test Data Factories
// test/factories/booking.factory.ts
import { faker } from '@faker-js/faker'
export function buildPassenger(overrides = {}) {
return {
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
dateOfBirth: faker.date.birthdate({ min: 18, max: 65, mode: 'age' }),
passengerType: 'ADULT',
...overrides
}
}
export function buildBookingInput(overrides = {}) {
return {
passengers: [buildPassenger()],
contact: {
email: faker.internet.email(),
phone: faker.phone.number()
},
...overrides
}
}
export async function createTicketedBooking(overrides = {}) {
const booking = await api.createBooking(buildBookingInput(overrides))
await api.processPayment(booking.id, { token: 'tok_visa_test' })
return api.getBooking(booking.pnr)
}
CI/CD Integration
GitHub Actions Workflow
# .github/workflows/full-flight-test.yml
name: Full Flight Simulation
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run typecheck
- run: npm run lint
- run: npm run format:check
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run test:unit -- --coverage
- uses: codecov/codecov-action@v3
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: aura_test
ports:
- 5432:5432
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_test
- run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_test
REDIS_URL: redis://localhost:6379
e2e-full-journey:
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
POSTGRES_DB: aura_e2e
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_e2e
# Run full flight simulation
- name: Full Passenger Journey Tests
run: npm run test:e2e:journey
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_e2e
# Run all edge cases
- name: Edge Case Tests
run: npm run test:e2e:edge-cases
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_e2e
# Run disruption scenarios
- name: Disruption & Recovery Tests
run: npm run test:e2e:disruption
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_e2e
# Run load tests
- name: Concurrency Tests
run: npm run test:e2e:load
env:
DATABASE_URL: postgresql://postgres:test@localhost:5432/aura_e2e
# Gate - All tests must pass
test-gate:
runs-on: ubuntu-latest
needs: [static-analysis, unit-tests, integration-tests, e2e-full-journey]
steps:
- run: echo "All tests passed - PR is ready for review"
Test Scripts
// package.json
{
"scripts": {
"test": "npm run test:unit && npm run test:integration && npm run test:e2e",
"test:unit": "vitest run --project unit",
"test:integration": "vitest run --project integration",
"test:e2e": "vitest run --project e2e",
"test:e2e:journey": "vitest run --project e2e --testPathPattern=journey",
"test:e2e:edge-cases": "vitest run --project e2e --testPathPattern=edge-cases",
"test:e2e:disruption": "vitest run --project e2e --testPathPattern=disruption",
"test:e2e:load": "vitest run --project e2e --testPathPattern=load",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage"
}
}
Test Coverage Requirements
Minimum Coverage Gates
| Layer | Minimum Coverage |
|---|---|
| Unit Tests | 80% |
| Integration Tests | 70% |
| E2E Journey Tests | 100% of happy paths |
| Edge Cases | 50+ scenarios |
Critical Path Coverage (Must be 100%)
- Booking creation flow
- Payment processing
- Ticket issuance
- Check-in (web + kiosk)
- Boarding pass generation
- Boarding gate scan
- Inventory management
- Cancellation & refunds
- Disruption rebooking
Research Topics
- Vitest vs Jest for parallel test execution
- Test containers for isolated database instances
- Playwright for kiosk/web UI testing
- k6 for load testing integration
- Test data generation strategies
- Snapshot testing for API contracts
- Visual regression testing for boarding passes
- Chaos engineering integration (Gremlin/Litmus)