@@ -87,8 +87,6 @@ start_broker() {
8787 wait $broker_pid 2> /dev/null || true
8888 broker_pid=$no_pid
8989 fi
90- generate_port
91- local tls_args=" "
9290 local has_tls=0
9391 local has_s_port=0
9492 for arg in " $@ " ; do
@@ -98,13 +96,30 @@ start_broker() {
9896 has_s_port=1
9997 fi
10098 done
101- if [ " $has_tls " -eq 1 ] && [ " $has_s_port " -eq 0 ]; then
102- tls_args=" -s $port "
103- fi
104- broker_log=" ${TMP_DIR} /broker_p${port} .log"
105- ./$broker_bin " $@ " -p $port $tls_args > " $broker_log " 2>&1 &
106- broker_pid=$!
107- check_broker
99+ # Retry a few times to absorb random-port collisions with
100+ # unrelated processes on this host (the broker binds without
101+ # SO_REUSEADDR, so a previous PID lingering in TIME_WAIT on the
102+ # same port also lands us here).
103+ local attempt=0
104+ while [ $attempt -lt 5 ]; do
105+ generate_port
106+ local tls_args=" "
107+ if [ " $has_tls " -eq 1 ] && [ " $has_s_port " -eq 0 ]; then
108+ tls_args=" -s $port "
109+ fi
110+ broker_log=" ${TMP_DIR} /broker_p${port} .log"
111+ ./$broker_bin " $@ " -p $port $tls_args > " $broker_log " 2>&1 &
112+ broker_pid=$!
113+ if check_broker; then
114+ return 0
115+ fi
116+ kill $broker_pid 2> /dev/null
117+ wait $broker_pid 2> /dev/null || true
118+ broker_pid=$no_pid
119+ attempt=$(( attempt + 1 ))
120+ done
121+ echo " WARNING: broker failed to start after 5 attempts" >&2
122+ return 1
108123}
109124
110125# Start broker with dual ports (plain + TLS).
@@ -1210,12 +1225,21 @@ fi # has_persist + has_retained
12101225# --- Test 28: Persist schema-mismatch wipe-and-restart ---
12111226# Corrupt a persisted record by stomping its schema_ver bytes. The
12121227# broker must log "schema mismatch", overwrite META, and start cleanly.
1228+ # Skipped in encrypted-persist builds: an attacker-supplied plaintext
1229+ # bogus record gets rejected by the decrypt stage before the schema
1230+ # check, so the wipe path is exercised via the plaintext build matrix
1231+ # entry instead.
12131232echo " "
12141233echo " --- Test 28: Persist schema mismatch ---"
1234+ t28_persist_encrypt=no
1235+ echo " $broker_features " | grep -q " persist-encrypt" && \
1236+ t28_persist_encrypt=yes
12151237if [ " $skip_plain " = " yes" ]; then
12161238 echo " SKIP: Persist schema mismatch (plain listener disabled)"
12171239elif [ " $has_persist " = " no" ]; then
12181240 echo " SKIP: Persist schema mismatch (built without --enable-broker-persist)"
1241+ elif [ " $t28_persist_encrypt " = " yes" ]; then
1242+ echo " SKIP: Persist schema mismatch (encrypted build - covered by plaintext matrix entry)"
12191243else
12201244T28_DIR=" ${TMP_DIR} /persist_t28"
12211245mkdir -p " $T28_DIR /1"
@@ -1247,6 +1271,261 @@ else
12471271fi
12481272fi # has_persist
12491273
1274+ # --- Test 29: Offline queue across reconnect (same broker) ---
1275+ # A persistent sub disconnects, publisher sends QoS 1 messages, sub
1276+ # reconnects and receives the queue. Exercises the orphan-session pool
1277+ # without going through a broker restart.
1278+ echo " "
1279+ echo " --- Test 29: Offline queue across reconnect ---"
1280+ if [ " $skip_plain " = " yes" ]; then
1281+ echo " SKIP: Offline queue (plain listener disabled)"
1282+ elif [ " $has_persist " = " no" ]; then
1283+ echo " SKIP: Offline queue (built without --enable-broker-persist)"
1284+ else
1285+ T29_DIR=" ${TMP_DIR} /persist_t29"
1286+ mkdir -p " $T29_DIR "
1287+ if [ $broker_pid != $no_pid ]; then
1288+ kill $broker_pid 2> /dev/null
1289+ wait $broker_pid 2> /dev/null || true
1290+ broker_pid=$no_pid
1291+ fi
1292+ generate_port
1293+ broker_log=" ${TMP_DIR} /t29_broker.log"
1294+ ./$broker_bin -p $port -D " $T29_DIR " > " $broker_log " 2>&1 &
1295+ broker_pid=$!
1296+ check_broker
1297+ # 1. Persistent sub connects, subscribes; SIGKILL to disconnect
1298+ # abruptly (graceful exit sends UNSUBSCRIBE which would tear down
1299+ # the subscription and defeat the orphan path).
1300+ rm -f " ${TMP_DIR} /t29_first.ready"
1301+ ./$sub_bin -T -h 127.0.0.1 -p $port -n " test/offlineq" -q 1 \
1302+ -i " t29_sub" -s \
1303+ -R " ${TMP_DIR} /t29_first.ready" \
1304+ > " ${TMP_DIR} /t29_first.log" 2>&1 &
1305+ T29_FIRST_PID=$!
1306+ TEST_PIDS+=($T29_FIRST_PID )
1307+ wait_for_file " ${TMP_DIR} /t29_first.ready" 5
1308+ kill -9 $T29_FIRST_PID 2> /dev/null
1309+ wait $T29_FIRST_PID 2> /dev/null || true
1310+ TEST_PIDS=()
1311+ sleep 0.5
1312+ # 2. Publish 3 QoS 1 messages while disconnected.
1313+ for t29_i in 1 2 3; do
1314+ ./$pub_bin -T -h 127.0.0.1 -p $port -n " test/offlineq" -q 1 \
1315+ -m " off_${t29_i} " -i " t29_pub_${t29_i} " \
1316+ >> " ${TMP_DIR} /t29_pub.log" 2>&1
1317+ done
1318+ sleep 0.5
1319+ # 3. Reconnect with same client_id + clean_session=0; receive backlog.
1320+ timeout 8 ./$sub_bin -T -h 127.0.0.1 -p $port -n " test/offlineq" -q 1 \
1321+ -i " t29_sub" -s -x -C 3 -R " ${TMP_DIR} /t29_sub.ready" \
1322+ > " ${TMP_DIR} /t29_sub.log" 2>&1 &
1323+ T29_PID=$!
1324+ TEST_PIDS+=($T29_PID )
1325+ wait_for_file " ${TMP_DIR} /t29_sub.ready" 5
1326+ sleep 1
1327+ kill $T29_PID 2> /dev/null
1328+ wait $T29_PID 2> /dev/null || true
1329+ TEST_PIDS=()
1330+ T29_RECV=$( grep -oE ' off_[0-9]+' " ${TMP_DIR} /t29_sub.log" 2> /dev/null \
1331+ | wc -l)
1332+ if [ " $T29_RECV " -ge 3 ]; then
1333+ echo " PASS: Offline queue across reconnect (received=$T29_RECV )"
1334+ else
1335+ echo " FAIL: Offline queue across reconnect (received=$T29_RECV expected>=3)"
1336+ FAIL=1
1337+ fi
1338+ fi # has_persist (t29)
1339+
1340+ # --- Test 30: Offline queue across broker restart ---
1341+ # Same as Test 29 but stops the broker between publish and reconnect to
1342+ # confirm NS_OUTQ records replay on restart.
1343+ echo " "
1344+ echo " --- Test 30: Offline queue across broker restart ---"
1345+ if [ " $skip_plain " = " yes" ]; then
1346+ echo " SKIP: Offline queue restart (plain listener disabled)"
1347+ elif [ " $has_persist " = " no" ]; then
1348+ echo " SKIP: Offline queue restart (built without --enable-broker-persist)"
1349+ else
1350+ T30_DIR=" ${TMP_DIR} /persist_t30"
1351+ mkdir -p " $T30_DIR "
1352+ if [ $broker_pid != $no_pid ]; then
1353+ kill $broker_pid 2> /dev/null
1354+ wait $broker_pid 2> /dev/null || true
1355+ broker_pid=$no_pid
1356+ fi
1357+ generate_port
1358+ broker_log=" ${TMP_DIR} /t30_broker1.log"
1359+ ./$broker_bin -p $port -D " $T30_DIR " > " $broker_log " 2>&1 &
1360+ broker_pid=$!
1361+ check_broker
1362+ rm -f " ${TMP_DIR} /t30_first.ready"
1363+ ./$sub_bin -T -h 127.0.0.1 -p $port -n " test/restartq" -q 1 \
1364+ -i " t30_sub" -s \
1365+ -R " ${TMP_DIR} /t30_first.ready" \
1366+ > " ${TMP_DIR} /t30_first.log" 2>&1 &
1367+ T30_FIRST_PID=$!
1368+ TEST_PIDS+=($T30_FIRST_PID )
1369+ wait_for_file " ${TMP_DIR} /t30_first.ready" 5
1370+ kill -9 $T30_FIRST_PID 2> /dev/null
1371+ wait $T30_FIRST_PID 2> /dev/null || true
1372+ TEST_PIDS=()
1373+ sleep 0.5
1374+ for t30_i in 1 2 3; do
1375+ ./$pub_bin -T -h 127.0.0.1 -p $port -n " test/restartq" -q 1 \
1376+ -m " rst_${t30_i} " -i " t30_pub_${t30_i} " \
1377+ >> " ${TMP_DIR} /t30_pub.log" 2>&1
1378+ done
1379+ sleep 0.5
1380+ # Restart broker against same directory
1381+ kill $broker_pid 2> /dev/null
1382+ wait $broker_pid 2> /dev/null || true
1383+ broker_pid=$no_pid
1384+ broker_log=" ${TMP_DIR} /t30_broker2.log"
1385+ ./$broker_bin -p $port -D " $T30_DIR " > " $broker_log " 2>&1 &
1386+ broker_pid=$!
1387+ check_broker
1388+ T30_REPLAY=no
1389+ grep -q " persist restore outq loaded=3" " $broker_log " 2> /dev/null \
1390+ && T30_REPLAY=yes
1391+ timeout 8 ./$sub_bin -T -h 127.0.0.1 -p $port -n " test/restartq" -q 1 \
1392+ -i " t30_sub" -s -x -C 3 -R " ${TMP_DIR} /t30_sub.ready" \
1393+ > " ${TMP_DIR} /t30_sub.log" 2>&1 &
1394+ T30_PID=$!
1395+ TEST_PIDS+=($T30_PID )
1396+ wait_for_file " ${TMP_DIR} /t30_sub.ready" 5
1397+ sleep 1
1398+ kill $T30_PID 2> /dev/null
1399+ wait $T30_PID 2> /dev/null || true
1400+ TEST_PIDS=()
1401+ T30_RECV=$( grep -oE ' rst_[0-9]+' " ${TMP_DIR} /t30_sub.log" 2> /dev/null \
1402+ | wc -l)
1403+ if [ " $T30_REPLAY " = " yes" ] && [ " $T30_RECV " -ge 3 ]; then
1404+ echo " PASS: Offline queue across restart (replay=$T30_REPLAY recv=$T30_RECV )"
1405+ else
1406+ echo " FAIL: Offline queue across restart (replay=$T30_REPLAY recv=$T30_RECV )"
1407+ FAIL=1
1408+ fi
1409+ fi # has_persist (t30)
1410+
1411+ # --- Test 31: AES-GCM encrypted records at rest ---
1412+ # Only meaningful when the broker is built with
1413+ # --enable-broker-persist-encrypt. Records on disk must not contain the
1414+ # plaintext payload string; round-trip must still work end-to-end.
1415+ echo " "
1416+ echo " --- Test 31: AES-GCM encrypted persist round-trip ---"
1417+ has_persist_encrypt=no
1418+ echo " $broker_features " | grep -q " persist-encrypt" && \
1419+ has_persist_encrypt=yes
1420+ if [ " $skip_plain " = " yes" ]; then
1421+ echo " SKIP: AES-GCM persist (plain listener disabled)"
1422+ elif [ " $has_persist_encrypt " = " no" ]; then
1423+ echo " SKIP: AES-GCM persist (built without --enable-broker-persist-encrypt)"
1424+ elif [ " $has_retained " = " no" ]; then
1425+ echo " SKIP: AES-GCM persist (retained support not built)"
1426+ else
1427+ T31_DIR=" ${TMP_DIR} /persist_t31"
1428+ mkdir -p " $T31_DIR "
1429+ if [ $broker_pid != $no_pid ]; then
1430+ kill $broker_pid 2> /dev/null
1431+ wait $broker_pid 2> /dev/null || true
1432+ broker_pid=$no_pid
1433+ fi
1434+ generate_port
1435+ broker_log=" ${TMP_DIR} /t31_broker1.log"
1436+ ./$broker_bin -p $port -D " $T31_DIR " > " $broker_log " 2>&1 &
1437+ broker_pid=$!
1438+ check_broker
1439+ ./$pub_bin -T -h 127.0.0.1 -p $port -n " test/secret_t31" \
1440+ -m " t31_secret_payload" -r > " ${TMP_DIR} /t31_pub.log" 2>&1
1441+ sleep 0.3
1442+ # On-disk record must NOT contain the plaintext string.
1443+ T31_LEAK=no
1444+ if grep -ql " t31_secret_payload" " $T31_DIR " /4/* .bin 2> /dev/null; then
1445+ T31_LEAK=yes
1446+ fi
1447+ # Restart broker - decryption + retained replay should still work.
1448+ kill $broker_pid 2> /dev/null
1449+ wait $broker_pid 2> /dev/null || true
1450+ broker_pid=$no_pid
1451+ broker_log=" ${TMP_DIR} /t31_broker2.log"
1452+ ./$broker_bin -p $port -D " $T31_DIR " > " $broker_log " 2>&1 &
1453+ broker_pid=$!
1454+ check_broker
1455+ rm -f " ${TMP_DIR} /t31_sub.ready"
1456+ timeout 5 ./$sub_bin -T -h 127.0.0.1 -p $port -n " test/secret_t31" \
1457+ -i " t31_sub" -R " ${TMP_DIR} /t31_sub.ready" \
1458+ > " ${TMP_DIR} /t31_sub.log" 2>&1 &
1459+ T31_PID=$!
1460+ TEST_PIDS+=($T31_PID )
1461+ wait_for_file " ${TMP_DIR} /t31_sub.ready" 3
1462+ sleep 0.5
1463+ kill $T31_PID 2> /dev/null
1464+ wait $T31_PID 2> /dev/null || true
1465+ TEST_PIDS=()
1466+ T31_RECV=no
1467+ grep -q " t31_secret_payload" " ${TMP_DIR} /t31_sub.log" 2> /dev/null \
1468+ && T31_RECV=yes
1469+ if [ " $T31_LEAK " = " no" ] && [ " $T31_RECV " = " yes" ]; then
1470+ echo " PASS: AES-GCM persist (no-leak + decrypt round-trip)"
1471+ else
1472+ echo " FAIL: AES-GCM persist (leak=$T31_LEAK recv=$T31_RECV )"
1473+ FAIL=1
1474+ fi
1475+ fi # has_persist_encrypt
1476+
1477+ # --- Test 32: Schema-mismatch wipe deletes every stale file ---
1478+ # Test 28 already verifies that a single bogus META makes the broker
1479+ # overwrite META, but doesn't check that OTHER namespaces get cleaned.
1480+ # This test plants stray files in every namespace and confirms they all
1481+ # disappear after the broker comes up.
1482+ echo " "
1483+ echo " --- Test 32: Schema mismatch wipes every namespace ---"
1484+ t32_persist_encrypt=no
1485+ echo " $broker_features " | grep -q " persist-encrypt" && \
1486+ t32_persist_encrypt=yes
1487+ if [ " $skip_plain " = " yes" ]; then
1488+ echo " SKIP: Schema wipe full (plain listener disabled)"
1489+ elif [ " $has_persist " = " no" ]; then
1490+ echo " SKIP: Schema wipe full (built without --enable-broker-persist)"
1491+ elif [ " $t32_persist_encrypt " = " yes" ]; then
1492+ echo " SKIP: Schema wipe full (encrypted build - exercised by plaintext matrix entry)"
1493+ else
1494+ T32_DIR=" ${TMP_DIR} /persist_t32"
1495+ rm -rf " $T32_DIR "
1496+ mkdir -p " $T32_DIR /1" " $T32_DIR /2" " $T32_DIR /3" " $T32_DIR /4" " $T32_DIR /5"
1497+ # Bogus META with wrong schema version (0xFFFF).
1498+ printf ' WMQB\xff\xff\x00\x01\x00\x00\x00\x04zzzz' > " $T32_DIR /1/00.bin"
1499+ # Plant a stray file in each other namespace.
1500+ echo " stray-session" > " $T32_DIR /2/aaaa.bin"
1501+ echo " stray-subs" > " $T32_DIR /3/bbbb.bin"
1502+ echo " stray-ret" > " $T32_DIR /4/cccc.bin"
1503+ echo " stray-outq" > " $T32_DIR /5/dddd.bin"
1504+ T32_BEFORE=$( find " $T32_DIR " -type f -name ' *.bin' | wc -l)
1505+ if [ $broker_pid != $no_pid ]; then
1506+ kill $broker_pid 2> /dev/null
1507+ wait $broker_pid 2> /dev/null || true
1508+ broker_pid=$no_pid
1509+ fi
1510+ generate_port
1511+ broker_log=" ${TMP_DIR} /t32_broker.log"
1512+ ./$broker_bin -p $port -D " $T32_DIR " > " $broker_log " 2>&1 &
1513+ broker_pid=$!
1514+ check_broker
1515+ # After wipe-and-restart only the fresh META record should remain.
1516+ T32_AFTER=$( find " $T32_DIR " -type f -name ' *.bin' | wc -l)
1517+ T32_WIPED=no
1518+ grep -q " persist wipe deleted" " $broker_log " 2> /dev/null \
1519+ && T32_WIPED=yes
1520+ if [ " $T32_BEFORE " -eq 5 ] && [ " $T32_AFTER " -eq 1 ] && \
1521+ [ " $T32_WIPED " = " yes" ]; then
1522+ echo " PASS: Schema wipe (before=$T32_BEFORE after=$T32_AFTER )"
1523+ else
1524+ echo " FAIL: Schema wipe (before=$T32_BEFORE after=$T32_AFTER wiped=$T32_WIPED )"
1525+ FAIL=1
1526+ fi
1527+ fi # has_persist (t32)
1528+
12501529# --- WebSocket Tests ---
12511530ws_client_bin=" examples/websocket/websocket_client"
12521531has_websocket=no
0 commit comments