Skip to content

Commit a42ccca

Browse files
author
jim
committed
update script
1 parent 0ca4395 commit a42ccca

1 file changed

Lines changed: 241 additions & 104 deletions

File tree

bookingSystem/scripts/OverbookingE2ETest.sh

Lines changed: 241 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
# =============================================================================
33
# OverbookingE2ETest.sh — Proves Redis atomic DECR prevents overbooking
44
#
5-
# What this script does:
6-
# 1. Creates a resource with capacity=1
7-
# 2. Fires N concurrent booking requests for the SAME time slot
8-
# 3. Asserts exactly 1 succeeded (HTTP 201) and the rest were rejected (HTTP 409)
9-
# 4. Cancels the successful booking and re-fires to prove the slot is restored
5+
# Scenarios:
6+
# 1. Capacity=1 — N concurrent requests, exactly 1 wins
7+
# 2. Capacity=3 — N concurrent requests, exactly 3 win
8+
# 3. Non-overlapping slots — same resource, different windows, both succeed
9+
# 4. Partial overlap — slot that partially overlaps is rejected
10+
# 5. Cancel restores slot — cancelled booking frees capacity for a new one
11+
# 6. Resource isolation — overbooking on resource A does not affect resource B
12+
# 7. Idempotency — same Idempotency-Key replayed returns 201 without double-booking
1013
#
1114
# Prerequisites:
1215
# - App running on localhost:8080 (mvn spring-boot:run)
@@ -15,116 +18,250 @@
1518
# =============================================================================
1619

1720
BASE_URL="http://localhost:8080"
18-
CONCURRENT=10 # Number of simultaneous booking attempts
19-
SLOT_START="2030-12-01T10:00:00"
20-
SLOT_END="2030-12-01T11:00:00"
21+
CONCURRENT=10
2122

2223
RED='\033[0;31m'
2324
GREEN='\033[0;32m'
2425
YELLOW='\033[1;33m'
26+
CYAN='\033[0;36m'
2527
NC='\033[0m'
2628

27-
pass() { echo -e "${GREEN}[PASS]${NC} $*"; }
28-
fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; }
29-
info() { echo -e "${YELLOW}[INFO]${NC} $*"; }
29+
PASS_COUNT=0
30+
FAIL_COUNT=0
3031

31-
# ---------------------------------------------------------------------------
32-
# 0. Verify server is up
33-
# ---------------------------------------------------------------------------
32+
pass() { echo -e " ${GREEN}[PASS]${NC} $*"; PASS_COUNT=$((PASS_COUNT+1)); }
33+
fail() { echo -e " ${RED}[FAIL]${NC} $*"; FAIL_COUNT=$((FAIL_COUNT+1)); }
34+
info() { echo -e " ${YELLOW}[INFO]${NC} $*"; }
35+
section() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; }
36+
37+
assert_eq() {
38+
local label="$1" actual="$2" expected="$3"
39+
if [[ "$actual" == "$expected" ]]; then
40+
pass "$label: $actual"
41+
else
42+
fail "$label: expected $expected, got $actual"
43+
fi
44+
}
45+
46+
# Fire $1 concurrent POST requests to the same slot on resource $2.
47+
# Writes HTTP codes to $TMPDIR/<n>. Returns after all finish.
48+
fire_concurrent() {
49+
local count=$1 resource_id=$2 start=$3 end=$4
50+
local tmp
51+
tmp=$(mktemp -d)
52+
53+
for i in $(seq 1 "$count"); do
54+
(
55+
code=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
56+
-H "Content-Type: application/json" \
57+
-d "{\"resourceId\":\"$resource_id\",\"userId\":\"user-$i\",\"startTime\":\"$start\",\"endTime\":\"$end\"}")
58+
echo "$code" > "$tmp/$i"
59+
) &
60+
done
61+
wait
62+
63+
local ok=0 conflict=0 other=0
64+
for i in $(seq 1 "$count"); do
65+
code=$(cat "$tmp/$i")
66+
case "$code" in
67+
201) ok=$((ok+1)) ;;
68+
409) conflict=$((conflict+1)) ;;
69+
*) other=$((other+1)); info "Unexpected HTTP $code from request $i" ;;
70+
esac
71+
done
72+
rm -rf "$tmp"
73+
74+
# Return via globals
75+
FIRE_OK=$ok
76+
FIRE_CONFLICT=$conflict
77+
FIRE_OTHER=$other
78+
}
79+
80+
create_resource() {
81+
local name=$1 capacity=$2
82+
local body
83+
body=$(curl -sf -X POST "$BASE_URL/api/resources" \
84+
-H "Content-Type: application/json" \
85+
-d "{\"name\":\"$name\",\"type\":\"room\",\"capacity\":$capacity,\"active\":true}")
86+
echo "$body" | jq -r '.id'
87+
}
88+
89+
confirmed_booking_id() {
90+
local resource_id=$1
91+
curl -sf "$BASE_URL/api/bookings?resourceId=$resource_id&size=20" \
92+
| jq -r '.content[] | select(.status=="CONFIRMED") | .id' | head -1
93+
}
94+
95+
cancel_booking() {
96+
local booking_id=$1
97+
curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE_URL/api/bookings/$booking_id"
98+
}
99+
100+
# =============================================================================
101+
# Preflight
102+
# =============================================================================
103+
section "Preflight"
34104
info "Checking server at $BASE_URL ..."
35-
curl -sf "$BASE_URL/api/bookings" -o /dev/null || fail "Server not reachable. Start the app first."
105+
curl -sf "$BASE_URL/api/bookings" -o /dev/null || { echo -e "${RED}[FATAL]${NC} Server not reachable."; exit 1; }
106+
pass "Server is up"
107+
108+
# =============================================================================
109+
# Scenario 1 — Capacity=1: only 1 of N concurrent requests succeeds
110+
# =============================================================================
111+
section "Scenario 1: Capacity=1 — $CONCURRENT concurrent requests, exactly 1 wins"
112+
113+
RES1=$(create_resource "E2E-Room-Cap1" 1)
114+
[[ -z "$RES1" || "$RES1" == "null" ]] && { fail "Failed to create resource"; exit 1; }
115+
info "Resource: $RES1 (capacity=1)"
116+
117+
fire_concurrent $CONCURRENT "$RES1" "2030-01-01T10:00:00" "2030-01-01T11:00:00"
118+
info "$CONCURRENT requests → 201:$FIRE_OK 409:$FIRE_CONFLICT other:$FIRE_OTHER"
119+
assert_eq "Successful bookings" "$FIRE_OK" "1"
120+
assert_eq "Rejected bookings" "$FIRE_CONFLICT" "$((CONCURRENT-1))"
121+
122+
# =============================================================================
123+
# Scenario 2 — Capacity=3: exactly 3 of N concurrent requests succeed
124+
# =============================================================================
125+
section "Scenario 2: Capacity=3 — $CONCURRENT concurrent requests, exactly 3 win"
126+
127+
RES2=$(create_resource "E2E-Room-Cap3" 3)
128+
info "Resource: $RES2 (capacity=3)"
36129

37-
# ---------------------------------------------------------------------------
38-
# 1. Create a resource with capacity=1
39-
# ---------------------------------------------------------------------------
40-
info "Creating resource with capacity=1 ..."
41-
RESOURCE=$(curl -sf -X POST "$BASE_URL/api/resources" \
130+
fire_concurrent $CONCURRENT "$RES2" "2030-02-01T10:00:00" "2030-02-01T11:00:00"
131+
info "$CONCURRENT requests → 201:$FIRE_OK 409:$FIRE_CONFLICT other:$FIRE_OTHER"
132+
assert_eq "Successful bookings" "$FIRE_OK" "3"
133+
assert_eq "Rejected bookings" "$FIRE_CONFLICT" "$((CONCURRENT-3))"
134+
135+
# =============================================================================
136+
# Scenario 3 — Non-overlapping slots on the same resource both succeed
137+
# =============================================================================
138+
section "Scenario 3: Non-overlapping slots — both should succeed"
139+
140+
RES3=$(create_resource "E2E-Room-Slots" 1)
141+
info "Resource: $RES3 (capacity=1)"
142+
143+
CODE_A=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
42144
-H "Content-Type: application/json" \
43-
-d '{"name":"Test Room E2E","type":"room","capacity":1,"active":true}')
44-
45-
RESOURCE_ID=$(echo "$RESOURCE" | jq -r '.id')
46-
[[ -z "$RESOURCE_ID" || "$RESOURCE_ID" == "null" ]] && fail "Failed to create resource: $RESOURCE"
47-
pass "Resource created: $RESOURCE_ID (capacity=1)"
48-
49-
# ---------------------------------------------------------------------------
50-
# 2. Fire N concurrent booking requests for the same slot
51-
# ---------------------------------------------------------------------------
52-
info "Firing $CONCURRENT concurrent booking requests for the same time slot ..."
53-
54-
TMPDIR_RESULTS=$(mktemp -d)
55-
56-
for i in $(seq 1 $CONCURRENT); do
57-
(
58-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
59-
-H "Content-Type: application/json" \
60-
-d "{
61-
\"resourceId\": \"$RESOURCE_ID\",
62-
\"userId\": \"user-$i\",
63-
\"startTime\": \"$SLOT_START\",
64-
\"endTime\": \"$SLOT_END\"
65-
}")
66-
echo "$HTTP_CODE" > "$TMPDIR_RESULTS/$i"
67-
) &
68-
done
69-
wait
70-
71-
# ---------------------------------------------------------------------------
72-
# 3. Count results
73-
# ---------------------------------------------------------------------------
74-
SUCCESS=0
75-
CONFLICT=0
76-
OTHER=0
77-
BOOKING_ID=""
78-
79-
for i in $(seq 1 $CONCURRENT); do
80-
CODE=$(cat "$TMPDIR_RESULTS/$i")
81-
case "$CODE" in
82-
201) SUCCESS=$((SUCCESS + 1)) ;;
83-
409) CONFLICT=$((CONFLICT + 1)) ;;
84-
*) OTHER=$((OTHER + 1)); info "Unexpected HTTP $CODE from request $i" ;;
85-
esac
86-
done
87-
rm -rf "$TMPDIR_RESULTS"
145+
-d "{\"resourceId\":\"$RES3\",\"userId\":\"alice\",\"startTime\":\"2030-03-01T09:00:00\",\"endTime\":\"2030-03-01T10:00:00\"}")
88146

89-
echo ""
90-
info "Results: $CONCURRENT concurrent requests →"
91-
echo " HTTP 201 (booked): $SUCCESS"
92-
echo " HTTP 409 (rejected): $CONFLICT"
93-
echo " Other: $OTHER"
94-
echo ""
147+
CODE_B=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
148+
-H "Content-Type: application/json" \
149+
-d "{\"resourceId\":\"$RES3\",\"userId\":\"bob\",\"startTime\":\"2030-03-01T11:00:00\",\"endTime\":\"2030-03-01T12:00:00\"}")
150+
151+
info "Slot A (09-10): HTTP $CODE_A"
152+
info "Slot B (11-12): HTTP $CODE_B"
153+
assert_eq "Slot A (09-10)" "$CODE_A" "201"
154+
assert_eq "Slot B (11-12)" "$CODE_B" "201"
155+
156+
# =============================================================================
157+
# Scenario 4 — Partial overlap is rejected
158+
# =============================================================================
159+
section "Scenario 4: Partial time overlap — second booking must be rejected"
160+
161+
RES4=$(create_resource "E2E-Room-Overlap" 1)
162+
info "Resource: $RES4 (capacity=1)"
163+
164+
CODE_FIRST=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
165+
-H "Content-Type: application/json" \
166+
-d "{\"resourceId\":\"$RES4\",\"userId\":\"alice\",\"startTime\":\"2030-04-01T10:00:00\",\"endTime\":\"2030-04-01T12:00:00\"}")
167+
168+
# Overlaps the tail of the first booking (10-12)
169+
CODE_OVERLAP=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
170+
-H "Content-Type: application/json" \
171+
-d "{\"resourceId\":\"$RES4\",\"userId\":\"bob\",\"startTime\":\"2030-04-01T11:00:00\",\"endTime\":\"2030-04-01T13:00:00\"}")
172+
173+
info "First booking (10-12): HTTP $CODE_FIRST"
174+
info "Overlapping booking (11-13): HTTP $CODE_OVERLAP"
175+
assert_eq "First booking" "$CODE_FIRST" "201"
176+
assert_eq "Overlapping booking" "$CODE_OVERLAP" "409"
177+
178+
# =============================================================================
179+
# Scenario 5 — Cancel restores the Redis slot for a new booking
180+
# =============================================================================
181+
section "Scenario 5: Cancel restores slot — next booking should succeed"
182+
183+
RES5=$(create_resource "E2E-Room-Cancel" 1)
184+
info "Resource: $RES5 (capacity=1)"
185+
186+
CODE_ORIG=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
187+
-H "Content-Type: application/json" \
188+
-d "{\"resourceId\":\"$RES5\",\"userId\":\"alice\",\"startTime\":\"2030-05-01T10:00:00\",\"endTime\":\"2030-05-01T11:00:00\"}")
189+
assert_eq "Original booking" "$CODE_ORIG" "201"
190+
191+
# Capacity exhausted — another user should be rejected
192+
CODE_REJECTED=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
193+
-H "Content-Type: application/json" \
194+
-d "{\"resourceId\":\"$RES5\",\"userId\":\"bob\",\"startTime\":\"2030-05-01T10:00:00\",\"endTime\":\"2030-05-01T11:00:00\"}")
195+
assert_eq "Second attempt (should fail)" "$CODE_REJECTED" "409"
196+
197+
BID5=$(confirmed_booking_id "$RES5")
198+
[[ -z "$BID5" || "$BID5" == "null" ]] && fail "No confirmed booking found for $RES5" || info "Cancelling booking $BID5 ..."
199+
CANCEL_CODE=$(cancel_booking "$BID5")
200+
assert_eq "Cancel" "$CANCEL_CODE" "204"
201+
202+
# Slot restored — next booking should succeed
203+
CODE_AFTER=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
204+
-H "Content-Type: application/json" \
205+
-d "{\"resourceId\":\"$RES5\",\"userId\":\"charlie\",\"startTime\":\"2030-05-01T10:00:00\",\"endTime\":\"2030-05-01T11:00:00\"}")
206+
assert_eq "Booking after cancel (slot restored)" "$CODE_AFTER" "201"
207+
208+
# =============================================================================
209+
# Scenario 6 — Resource isolation: overbooking A does not affect B
210+
# =============================================================================
211+
section "Scenario 6: Resource isolation — exhausting A leaves B unaffected"
212+
213+
RES6A=$(create_resource "E2E-Room-IsoA" 1)
214+
RES6B=$(create_resource "E2E-Room-IsoB" 1)
215+
info "Resource A: $RES6A | Resource B: $RES6B"
216+
217+
# Exhaust resource A
218+
fire_concurrent $CONCURRENT "$RES6A" "2030-06-01T10:00:00" "2030-06-01T11:00:00"
219+
assert_eq "Resource A: only 1 wins" "$FIRE_OK" "1"
95220

96-
[[ $SUCCESS -eq 1 ]] && pass "Exactly 1 booking succeeded" || fail "Expected 1 success, got $SUCCESS"
97-
[[ $CONFLICT -eq $((CONCURRENT - 1)) ]] && pass "$CONFLICT requests correctly rejected" || fail "Expected $((CONCURRENT-1)) conflicts, got $CONFLICT"
98-
99-
# ---------------------------------------------------------------------------
100-
# 4. Fetch the booking ID of the successful booking
101-
# ---------------------------------------------------------------------------
102-
BOOKING_PAGE=$(curl -sf "$BASE_URL/api/bookings?resourceId=$RESOURCE_ID&size=10")
103-
BOOKING_ID=$(echo "$BOOKING_PAGE" | jq -r '.content[] | select(.status=="CONFIRMED") | .id' | head -1)
104-
[[ -z "$BOOKING_ID" || "$BOOKING_ID" == "null" ]] && fail "Could not find the confirmed booking"
105-
pass "Confirmed booking ID: $BOOKING_ID"
106-
107-
# ---------------------------------------------------------------------------
108-
# 5. Cancel the booking — Redis slot should be restored (INCR)
109-
# ---------------------------------------------------------------------------
110-
info "Cancelling booking to restore the Redis slot ..."
111-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE_URL/api/bookings/$BOOKING_ID")
112-
[[ "$HTTP_CODE" == "204" ]] && pass "Booking cancelled (HTTP 204)" || fail "Cancel failed (HTTP $HTTP_CODE)"
113-
114-
# ---------------------------------------------------------------------------
115-
# 6. Retry a single booking — should succeed now that slot is restored
116-
# ---------------------------------------------------------------------------
117-
info "Retrying a single booking after cancellation ..."
118-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
221+
# Resource B should still accept a booking independently
222+
CODE_B=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
119223
-H "Content-Type: application/json" \
120-
-d "{
121-
\"resourceId\": \"$RESOURCE_ID\",
122-
\"userId\": \"user-retry\",
123-
\"startTime\": \"$SLOT_START\",
124-
\"endTime\": \"$SLOT_END\"
125-
}")
126-
[[ "$HTTP_CODE" == "201" ]] && pass "Slot restored — new booking succeeded (HTTP 201)" || fail "Expected 201 after cancel, got HTTP $HTTP_CODE"
127-
128-
# ---------------------------------------------------------------------------
224+
-d "{\"resourceId\":\"$RES6B\",\"userId\":\"dave\",\"startTime\":\"2030-06-01T10:00:00\",\"endTime\":\"2030-06-01T11:00:00\"}")
225+
assert_eq "Resource B unaffected" "$CODE_B" "201"
226+
227+
# =============================================================================
228+
# Scenario 7 — Idempotency: replaying the same key returns 201 without double-booking
229+
# =============================================================================
230+
section "Scenario 7: Idempotency key — replay must not consume an extra slot"
231+
232+
RES7=$(create_resource "E2E-Room-Idem" 1)
233+
info "Resource: $RES7 (capacity=1)"
234+
IDEM_KEY="test-idem-key-$(date +%s)"
235+
236+
BODY="{\"resourceId\":\"$RES7\",\"userId\":\"alice\",\"startTime\":\"2030-07-01T10:00:00\",\"endTime\":\"2030-07-01T11:00:00\"}"
237+
238+
CODE_FIRST=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
239+
-H "Content-Type: application/json" \
240+
-H "Idempotency-Key: $IDEM_KEY" \
241+
-d "$BODY")
242+
assert_eq "First request" "$CODE_FIRST" "201"
243+
244+
CODE_REPLAY=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
245+
-H "Content-Type: application/json" \
246+
-H "Idempotency-Key: $IDEM_KEY" \
247+
-d "$BODY")
248+
assert_eq "Replayed request (same key)" "$CODE_REPLAY" "201"
249+
250+
# After replay, capacity should still be 0 — a new user with no idempotency key must be rejected
251+
CODE_NEW=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$BASE_URL/api/bookings" \
252+
-H "Content-Type: application/json" \
253+
-d "{\"resourceId\":\"$RES7\",\"userId\":\"bob\",\"startTime\":\"2030-07-01T10:00:00\",\"endTime\":\"2030-07-01T11:00:00\"}")
254+
assert_eq "New request after replay (must fail)" "$CODE_NEW" "409"
255+
256+
# =============================================================================
257+
# Summary
258+
# =============================================================================
129259
echo ""
130-
pass "All assertions passed. Redis overbooking prevention is working correctly."
260+
echo -e "${CYAN}━━━ Summary ━━━${NC}"
261+
echo -e " ${GREEN}Passed: $PASS_COUNT${NC}"
262+
if [[ $FAIL_COUNT -gt 0 ]]; then
263+
echo -e " ${RED}Failed: $FAIL_COUNT${NC}"
264+
exit 1
265+
else
266+
echo -e " ${GREEN}All scenarios passed. Redis overbooking prevention is working correctly.${NC}"
267+
fi

0 commit comments

Comments
 (0)