|
2 | 2 | # ============================================================================= |
3 | 3 | # OverbookingE2ETest.sh — Proves Redis atomic DECR prevents overbooking |
4 | 4 | # |
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 |
10 | 13 | # |
11 | 14 | # Prerequisites: |
12 | 15 | # - App running on localhost:8080 (mvn spring-boot:run) |
|
15 | 18 | # ============================================================================= |
16 | 19 |
|
17 | 20 | 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 |
21 | 22 |
|
22 | 23 | RED='\033[0;31m' |
23 | 24 | GREEN='\033[0;32m' |
24 | 25 | YELLOW='\033[1;33m' |
| 26 | +CYAN='\033[0;36m' |
25 | 27 | NC='\033[0m' |
26 | 28 |
|
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 |
30 | 31 |
|
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" |
34 | 104 | 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)" |
36 | 129 |
|
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" \ |
42 | 144 | -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\"}") |
88 | 146 |
|
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" |
95 | 220 |
|
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" \ |
119 | 223 | -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 | +# ============================================================================= |
129 | 259 | 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