513 lines
27 KiB
Bash
Executable File
513 lines
27 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
BASE="https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/api/v1"
|
|
PASS=0 FAIL=0
|
|
CLEANUP_PRODUCT_IDS=()
|
|
CLEANUP_SALE_IDS=()
|
|
CLEANUP_COUPON_IDS=()
|
|
|
|
check() {
|
|
local label="$1" expect="$2" method="$3" path="$4"; shift 4
|
|
local code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null)
|
|
if [ "$code" = "$expect" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: $label — expected $expect got $code"; fi
|
|
}
|
|
check_field() {
|
|
local actual=$(jq -r "$2" /tmp/qa_body.json 2>/dev/null)
|
|
if [ "$actual" = "$3" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: $1 — expected '$3' got '$actual'"; fi
|
|
}
|
|
check_not_500() {
|
|
local label="$1" method="$2" path="$3"; shift 3
|
|
local code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -X "$method" "$BASE$path" "$@" 2>/dev/null)
|
|
if [ "$code" != "500" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: $label — got 500 Internal Server Error"; fi
|
|
}
|
|
check_true() {
|
|
local label="$1" expr="$2"
|
|
local result=$(jq "$expr" /tmp/qa_body.json 2>/dev/null)
|
|
if [ "$result" = "true" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: $label"; fi
|
|
}
|
|
|
|
echo "=== Logging in ==="
|
|
ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
|
|
STAFF_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"staff","password":"staff123"}' | jq -r '.token')
|
|
CUST_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"customer","password":"customer123"}' | jq -r '.token')
|
|
|
|
if [ "$ADMIN_TOKEN" = "null" ] || [ -z "$ADMIN_TOKEN" ]; then echo "FATAL: Admin login failed"; exit 1; fi
|
|
if [ "$STAFF_TOKEN" = "null" ] || [ -z "$STAFF_TOKEN" ]; then echo "FATAL: Staff login failed"; exit 1; fi
|
|
if [ "$CUST_TOKEN" = "null" ] || [ -z "$CUST_TOKEN" ]; then echo "FATAL: Customer login failed"; exit 1; fi
|
|
echo "All 3 logins OK"
|
|
|
|
A=(-H "Authorization: Bearer $ADMIN_TOKEN")
|
|
S=(-H "Authorization: Bearer $STAFF_TOKEN")
|
|
C=(-H "Authorization: Bearer $CUST_TOKEN")
|
|
J=(-H "Content-Type: application/json")
|
|
|
|
cleanup() {
|
|
echo ""
|
|
echo "=== Cleanup ==="
|
|
for pid in "${CLEANUP_PRODUCT_IDS[@]}"; do
|
|
curl -s -o /dev/null -X DELETE "$BASE/products/$pid" "${A[@]}" 2>/dev/null
|
|
done
|
|
for sid in "${CLEANUP_SALE_IDS[@]}"; do
|
|
curl -s -o /dev/null -X DELETE "$BASE/sales/$sid" "${A[@]}" 2>/dev/null
|
|
done
|
|
for cid in "${CLEANUP_COUPON_IDS[@]}"; do
|
|
curl -s -o /dev/null -X DELETE "$BASE/coupons/$cid" "${A[@]}" 2>/dev/null
|
|
done
|
|
echo "Cleanup done"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== C — CONFORMANCE =========="
|
|
###############################################################################
|
|
|
|
echo "--- Response format validation ---"
|
|
check "GET /products 200" 200 GET "/products"
|
|
check_true "products .content is array" '.content | type == "array"'
|
|
check_true "products has totalElements" '.totalElements != null'
|
|
check_true "products has totalPages" '.totalPages != null'
|
|
check_true "products has number" '.number != null'
|
|
check_true "products has size" '.size != null'
|
|
|
|
check "GET /products/1 200" 200 GET "/products/1"
|
|
check_true "prodPrice is number ≤2 decimals" '(.prodPrice | tostring | test("^[0-9]+(\\.[0-9]{1,2})?$"))'
|
|
|
|
check "GET /sales admin 200" 200 GET "/sales" "${A[@]}" "${J[@]}"
|
|
check_true "saleDate has ISO T" '(.content[0].saleDate | test("T"))'
|
|
|
|
check "GET /appointments admin 200" 200 GET "/appointments" "${A[@]}" "${J[@]}"
|
|
check_true "appointmentDate YYYY-MM-DD" '(.content[0].appointmentDate | test("^[0-9]{4}-[0-9]{2}-[0-9]{2}"))'
|
|
|
|
local_code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -D /tmp/qa_headers.txt -X GET "$BASE/auth/me" "${A[@]}" 2>/dev/null)
|
|
if grep -qi "application/json" /tmp/qa_headers.txt 2>/dev/null; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: /auth/me Content-Type not json"; fi
|
|
|
|
local_code=$(curl -s -o /tmp/qa_body.json -w "%{http_code}" -D /tmp/qa_headers.txt -X GET "$BASE/users/1/avatar/file" "${A[@]}" 2>/dev/null)
|
|
if grep -qi "image/" /tmp/qa_headers.txt 2>/dev/null; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: avatar Content-Type not image/"; fi
|
|
|
|
echo "--- Status enum validation ---"
|
|
check "GET /appointments enum" 200 GET "/appointments?size=50" "${A[@]}"
|
|
check_true "appointmentStatus valid" '[.content[].appointmentStatus] | all(. == "Scheduled" or . == "Completed" or . == "Cancelled" or . == "In Progress" or . == "No Show")'
|
|
|
|
check "GET /pets enum" 200 GET "/pets?size=100"
|
|
check_true "petStatus valid" '[.content[].petStatus] | all(. == "Available" or . == "Pending" or . == "Adopted" or . == "Owned")'
|
|
|
|
check "GET /sales enum" 200 GET "/sales?size=50" "${A[@]}"
|
|
check_true "channel valid" '[.content[].channel] | all(. == "IN_STORE" or . == "ONLINE")'
|
|
|
|
check "GET /users enum" 200 GET "/users?size=100" "${A[@]}"
|
|
check_true "role valid" '[.content[].role] | all(. == "ADMIN" or . == "STAFF" or . == "CUSTOMER")'
|
|
|
|
check "GET /coupons enum" 200 GET "/coupons" "${A[@]}"
|
|
check_true "discountType valid" '[.[].discountType] | all(. == "PERCENTAGE" or . == "FIXED")'
|
|
|
|
echo "--- Error format validation ---"
|
|
check "GET /products/999999 404" 404 GET "/products/999999"
|
|
check_true "404 has status" '.status != null'
|
|
check_true "404 has message" '.message != null'
|
|
check_true "404 has path" '.path != null'
|
|
check_true "404 has timestamp" '.timestamp != null'
|
|
|
|
echo "--- Input format enforcement ---"
|
|
check_not_500 "invalid JSON body" POST "/products" "${A[@]}" "${J[@]}" -d '{bad json}'
|
|
check_not_500 "text/plain content-type" POST "/products" "${A[@]}" -H "Content-Type: text/plain" -d 'hello'
|
|
check_not_500 "empty body" POST "/products" "${A[@]}" "${J[@]}" -d ''
|
|
check_not_500 "XML body" POST "/products" "${A[@]}" -H "Content-Type: application/xml" -d '<xml/>'
|
|
|
|
echo "--- Security conformance ---"
|
|
check_not_500 "SQL injection in search" GET "/products?q=%27%20OR%201%3D1--"
|
|
check_not_500 "XSS in search" GET '/products?q=<script>alert(1)</script>'
|
|
|
|
check_not_500 "XSS in prodName" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"<img src=x onerror=alert(1)>","prodPrice":9.99,"categoryId":1}'
|
|
XSS_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
|
|
if [ -n "$XSS_ID" ]; then
|
|
CLEANUP_PRODUCT_IDS+=("$XSS_ID")
|
|
check "GET XSS product" 200 GET "/products/$XSS_ID"
|
|
check_true "XSS stored literally" '(.prodName | contains("<img"))'
|
|
fi
|
|
|
|
LONG_NAME=$(printf 'A%.0s' $(seq 1 5000))
|
|
check "5000 char prodName → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"$LONG_NAME\",\"prodPrice\":9.99,\"categoryId\":1}"
|
|
|
|
check_not_500 "unicode prodName" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"Café Résumé","prodPrice":9.99,"categoryId":1}'
|
|
UNI_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
|
|
if [ -n "$UNI_ID" ]; then
|
|
CLEANUP_PRODUCT_IDS+=("$UNI_ID")
|
|
check "GET unicode product" 200 GET "/products/$UNI_ID"
|
|
check_field "unicode preserved" '.prodName' 'Café Résumé'
|
|
fi
|
|
|
|
check_not_500 "null byte in category" POST "/categories" "${A[@]}" "${J[@]}" -d '{"categoryName":"test\u0000inject"}'
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== O — ORDERING =========="
|
|
###############################################################################
|
|
|
|
check "products price asc" 200 GET "/products?sort=prodPrice,asc&size=100"
|
|
check_true "prices non-decreasing" '[.content[].prodPrice] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'
|
|
|
|
check "products price desc" 200 GET "/products?sort=prodPrice,desc&size=100"
|
|
check_true "prices non-increasing" '[.content[].prodPrice] | . as $a | [range(1;length)] | all(. as $i | $a[$i] <= $a[$i-1])'
|
|
|
|
check "products name asc" 200 GET "/products?sort=prodName,asc&size=100"
|
|
check_true "names alphabetical" '[.content[].prodName] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'
|
|
|
|
check "sales date desc" 200 GET "/sales?sort=saleDate,desc&size=50" "${A[@]}"
|
|
check_true "sale dates non-increasing" '[.content[].saleDate] | . as $a | [range(1;length)] | all(. as $i | $a[$i] <= $a[$i-1])'
|
|
|
|
check "users lastName asc" 200 GET "/users?sort=lastName,asc&size=100" "${A[@]}"
|
|
check_true "lastNames alphabetical" '[.content[].lastName] | . as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'
|
|
|
|
check_not_500 "sort by nonexistent field" GET "/products?sort=nonexistent,asc"
|
|
|
|
check "page 0 size 5" 200 GET "/products?page=0&size=5"
|
|
PAGE0_IDS=$(jq -r '[.content[].prodId] | join(",")' /tmp/qa_body.json)
|
|
check "page 1 size 5" 200 GET "/products?page=1&size=5"
|
|
PAGE1_IDS=$(jq -r '[.content[].prodId] | join(",")' /tmp/qa_body.json)
|
|
OVERLAP=0
|
|
IFS=',' read -ra P0 <<< "$PAGE0_IDS"
|
|
IFS=',' read -ra P1 <<< "$PAGE1_IDS"
|
|
for a in "${P0[@]}"; do for b in "${P1[@]}"; do [ "$a" = "$b" ] && OVERLAP=1; done; done
|
|
if [ "$OVERLAP" = "0" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: page 0 and page 1 overlap"; fi
|
|
|
|
check "conversations" 200 GET "/chat/conversations" "${C[@]}"
|
|
CONV_ID=$(jq -r '.[0].conversationId // empty' /tmp/qa_body.json 2>/dev/null)
|
|
if [ -n "$CONV_ID" ]; then
|
|
check "conv messages" 200 GET "/chat/conversations/$CONV_ID/messages" "${C[@]}"
|
|
check_true "message timestamps ascending" '. as $a | [range(1;length)] | all(. as $i | $a[$i].timestamp >= $a[$i-1].timestamp)'
|
|
fi
|
|
|
|
check "activity logs desc" 200 GET "/activity-logs?limit=10" "${A[@]}"
|
|
check_true "log timestamps descending" '. as $a | [range(1;length)] | all(. as $i | $a[$i].logTimestamp <= $a[$i-1].logTimestamp)'
|
|
|
|
check "availability times" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-06-01" "${A[@]}"
|
|
check_true "times ascending" '. as $a | [range(1;length)] | all(. as $i | $a[$i] >= $a[$i-1])'
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== R — RANGE =========="
|
|
###############################################################################
|
|
|
|
echo "--- Data range checks ---"
|
|
check "products range" 200 GET "/products?size=200"
|
|
check_true "all prodPrice > 0" '[.content[].prodPrice] | all(. > 0)'
|
|
|
|
check "services range" 200 GET "/services"
|
|
check_true "all servicePrice > 0" '[.[].servicePrice] | all(. > 0)'
|
|
check_true "all serviceDuration > 0" '[.[].serviceDuration] | all(. > 0)'
|
|
|
|
check "inventory range" 200 GET "/inventory" "${A[@]}"
|
|
check_true "all quantity >= 0" '[.content[].quantity] | all(. >= 0)'
|
|
|
|
check "users loyalty" 200 GET "/users?size=100" "${A[@]}"
|
|
check_true "all loyaltyPoints >= 0" '[.content[].loyaltyPoints] | all(. >= 0)'
|
|
|
|
check "coupons range" 200 GET "/coupons" "${A[@]}"
|
|
check_true "PERCENTAGE < 100 and > 0" '[.[] | select(.discountType == "PERCENTAGE") | .discountValue] | all(. > 0 and . < 100)'
|
|
|
|
check "pets age range" 200 GET "/pets?size=200"
|
|
check_true "all petAge >= 0" '[.content[].petAge | select(. != null)] | all(. >= 0)'
|
|
|
|
echo "--- Input range enforcement ---"
|
|
check "negative price → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_NEG","prodPrice":-1,"categoryId":1}'
|
|
NEG_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
|
|
[ -n "$NEG_ID" ] && CLEANUP_PRODUCT_IDS+=("$NEG_ID")
|
|
|
|
check "zero price → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_ZERO","prodPrice":0,"categoryId":1}'
|
|
Z_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
|
|
[ -n "$Z_ID" ] && CLEANUP_PRODUCT_IDS+=("$Z_ID")
|
|
|
|
check "zero duration → 400" 400 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_DUR0","servicePrice":10,"serviceDuration":0}'
|
|
check "neg duration → 400" 400 POST "/services" "${A[@]}" "${J[@]}" -d '{"serviceName":"QA_DURN","servicePrice":10,"serviceDuration":-5}'
|
|
|
|
check "neg cart qty → 400" 400 POST "/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":-1}'
|
|
|
|
check "zero coupon pct → 400" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"QA_ZERO_PCT","discountType":"PERCENTAGE","discountValue":0}'
|
|
|
|
check_not_500 "huge price" POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_HUGE","prodPrice":99999999.99,"categoryId":1}'
|
|
HUGE_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json 2>/dev/null)
|
|
[ -n "$HUGE_ID" ] && CLEANUP_PRODUCT_IDS+=("$HUGE_ID")
|
|
|
|
echo "--- Extreme pagination ---"
|
|
check_not_500 "page=-1" GET "/products?page=-1"
|
|
check_not_500 "size=0" GET "/products?size=0"
|
|
check_not_500 "size=10000" GET "/products?size=10000"
|
|
|
|
echo "--- String length ---"
|
|
LONG_USER=$(printf 'u%.0s' $(seq 1 60))
|
|
check "60-char username → 400" 400 POST "/auth/register" "${J[@]}" -d "{\"username\":\"$LONG_USER\",\"password\":\"pass1234\",\"firstName\":\"Q\",\"lastName\":\"A\",\"email\":\"${LONG_USER}@test.com\"}"
|
|
|
|
LONG_PNAME=$(printf 'p%.0s' $(seq 1 200))
|
|
check "200-char prodName → 400" 400 POST "/products" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"$LONG_PNAME\",\"prodPrice\":9.99,\"categoryId\":1}"
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== R — REFERENCE =========="
|
|
###############################################################################
|
|
|
|
echo "--- FK integrity: sales ---"
|
|
check "sales sample" 200 GET "/sales?size=3" "${A[@]}"
|
|
for i in 0 1 2; do
|
|
SID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_body.json)
|
|
EID=$(jq -r ".content[$i].employeeId // empty" /tmp/qa_body.json)
|
|
[ -n "$SID" ] && check "sale[$i] store $SID exists" 200 GET "/stores/$SID"
|
|
[ -n "$EID" ] && check "sale[$i] employee $EID exists" 200 GET "/users/$EID" "${A[@]}"
|
|
done
|
|
|
|
echo "--- FK integrity: appointments ---"
|
|
check "appts sample" 200 GET "/appointments?size=3" "${A[@]}"
|
|
cp /tmp/qa_body.json /tmp/qa_appts.json
|
|
for i in 0 1 2; do
|
|
SVID=$(jq -r ".content[$i].serviceId // empty" /tmp/qa_appts.json)
|
|
STID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_appts.json)
|
|
CID=$(jq -r ".content[$i].customerId // empty" /tmp/qa_appts.json)
|
|
EMID=$(jq -r ".content[$i].employeeId // empty" /tmp/qa_appts.json)
|
|
[ -n "$SVID" ] && check "appt[$i] service $SVID" 200 GET "/services/$SVID"
|
|
[ -n "$STID" ] && check "appt[$i] store $STID" 200 GET "/stores/$STID"
|
|
[ -n "$CID" ] && check "appt[$i] customer $CID" 200 GET "/users/$CID" "${A[@]}"
|
|
[ -n "$EMID" ] && check "appt[$i] employee $EMID" 200 GET "/users/$EMID" "${A[@]}"
|
|
done
|
|
|
|
echo "--- FK integrity: inventory ---"
|
|
check "inv sample" 200 GET "/inventory?size=3" "${A[@]}"
|
|
cp /tmp/qa_body.json /tmp/qa_inv.json
|
|
for i in 0 1 2; do
|
|
ISTID=$(jq -r ".content[$i].storeId // empty" /tmp/qa_inv.json)
|
|
IPID=$(jq -r ".content[$i].prodId // empty" /tmp/qa_inv.json)
|
|
[ -n "$ISTID" ] && check "inv[$i] store $ISTID" 200 GET "/stores/$ISTID"
|
|
[ -n "$IPID" ] && check "inv[$i] product $IPID" 200 GET "/products/$IPID"
|
|
done
|
|
|
|
echo "--- FK integrity: products ---"
|
|
check "products sample" 200 GET "/products?size=3"
|
|
cp /tmp/qa_body.json /tmp/qa_prods.json
|
|
for i in 0 1 2; do
|
|
CATID=$(jq -r ".content[$i].categoryId // empty" /tmp/qa_prods.json)
|
|
[ -n "$CATID" ] && check "prod[$i] category $CATID" 200 GET "/categories/$CATID"
|
|
done
|
|
|
|
echo "--- Cross-ref violations ---"
|
|
check "sale bad storeId" 404 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":999,"paymentMethod":"Cash","channel":"IN_STORE","items":[{"prodId":1,"quantity":1}]}'
|
|
check "appt bad serviceId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":999,"petId":53,"customerId":32,"storeId":2,"employeeId":7,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}'
|
|
check "appt bad customerId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":1,"petId":53,"customerId":999,"storeId":2,"employeeId":7,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}'
|
|
check "appt bad employeeId" 404 POST "/appointments" "${A[@]}" "${J[@]}" -d '{"serviceId":1,"petId":53,"customerId":32,"storeId":2,"employeeId":999,"appointmentDate":"2027-09-01","appointmentTime":"10:00","appointmentStatus":"Scheduled"}'
|
|
|
|
echo "--- Dangling reference (FK on delete) ---"
|
|
DEL_CAT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE/categories/1" "${A[@]}" 2>/dev/null)
|
|
if [ "$DEL_CAT_CODE" != "204" ] && [ "$DEL_CAT_CODE" != "200" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: DELETE /categories/1 should fail (FK) — got $DEL_CAT_CODE"; fi
|
|
|
|
DEL_STORE_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "$BASE/stores/1" "${A[@]}" 2>/dev/null)
|
|
if [ "$DEL_STORE_CODE" != "204" ] && [ "$DEL_STORE_CODE" != "200" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: DELETE /stores/1 should fail (FK) — got $DEL_STORE_CODE"; fi
|
|
|
|
echo "--- Avatar file references ---"
|
|
check "admin avatar file" 200 GET "/users/1/avatar/file" "${A[@]}"
|
|
check "user 16 avatar file" 200 GET "/users/16/avatar/file" "${A[@]}"
|
|
check "product 1 image" 200 GET "/products/1/image"
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== E — EXISTENCE =========="
|
|
###############################################################################
|
|
|
|
echo "--- Required fields never null ---"
|
|
check "products fields" 200 GET "/products?size=100"
|
|
check_true "products non-null fields" '.content | all(.prodId != null and .prodName != null and .prodPrice != null and .categoryId != null)'
|
|
|
|
check "users fields" 200 GET "/users?size=100" "${A[@]}"
|
|
check_true "users non-null fields" '.content | all(.id != null and .firstName != null and .lastName != null and .role != null)'
|
|
|
|
check "sales fields" 200 GET "/sales?size=50" "${A[@]}"
|
|
check_true "sales non-null fields" '.content | all(.saleId != null and .saleDate != null and .totalAmount != null and .storeId != null and .employeeId != null)'
|
|
|
|
check "appts fields" 200 GET "/appointments?size=50" "${A[@]}"
|
|
check_true "appts non-null fields" '.content | all(.appointmentId != null and .serviceId != null and .customerId != null and .storeId != null and .appointmentDate != null and .appointmentTime != null and .appointmentStatus != null)'
|
|
|
|
check "coupons fields" 200 GET "/coupons" "${A[@]}"
|
|
check_true "coupons non-null fields" '[.[] | .couponId != null and .couponCode != null and .discountType != null and .discountValue != null] | all'
|
|
|
|
echo "--- Avatar existence for all roles ---"
|
|
check "admin me avatar" 200 GET "/auth/me/avatar/file" "${A[@]}"
|
|
check "staff me avatar" 200 GET "/auth/me/avatar/file" "${S[@]}"
|
|
check "customer me avatar" 200 GET "/auth/me/avatar/file" "${C[@]}"
|
|
|
|
echo "--- Empty search returns valid structure ---"
|
|
check "empty product search" 200 GET "/products?q=zzzznothing"
|
|
check_true "empty search .content is array" '.content | type == "array"'
|
|
check_true "empty search totalElements 0" '.totalElements == 0'
|
|
|
|
check "empty pet species" 200 GET "/pets?species=Unicorn"
|
|
check_true "unicorn totalElements 0" '.totalElements == 0'
|
|
|
|
check "future appts" 200 GET "/appointments?date=2099-12-31" "${A[@]}"
|
|
check_true "far future totalElements 0" '.totalElements == 0'
|
|
|
|
check "nonexistent activity log" 200 GET "/activity-logs?search=xyznonexistent" "${A[@]}"
|
|
check_true "activity log empty array" 'length == 0'
|
|
|
|
echo "--- Missing parameter handling ---"
|
|
check_not_500 "cart no storeId" GET "/cart" "${C[@]}"
|
|
check_not_500 "availability no params" GET "/appointments/availability"
|
|
check_not_500 "checkout empty body" POST "/cart/checkout" "${C[@]}" "${J[@]}" -d '{}'
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== C — CARDINALITY =========="
|
|
###############################################################################
|
|
|
|
echo "--- Unique constraints ---"
|
|
check "dup username admin" 409 POST "/auth/register" "${J[@]}" -d '{"username":"admin","password":"pass1234","firstName":"Q","lastName":"A","email":"qadup1@test.com"}'
|
|
DUP_CODE=$(cat /tmp/qa_body.json | jq -r '.status // empty' 2>/dev/null)
|
|
if [ "$?" != "0" ] || [ "$(curl -s -o /dev/null -w "%{http_code}" /dev/null 2>/dev/null)" = "" ]; then true; fi
|
|
|
|
check "dup email admin" 409 POST "/auth/register" "${J[@]}" -d '{"username":"qauniq99","password":"pass1234","firstName":"Q","lastName":"A","email":"admin@petshop.com"}'
|
|
|
|
check "dup coupon code" 400 POST "/coupons" "${A[@]}" "${J[@]}" -d '{"couponCode":"WELCOME10","discountType":"PERCENTAGE","discountValue":10}'
|
|
|
|
echo "--- One-to-many counts ---"
|
|
check "sale 1 items" 200 GET "/sales/1" "${A[@]}"
|
|
check_true "sale has >= 1 item" '(.items | length) >= 1'
|
|
|
|
check "service 1 species" 200 GET "/services/1"
|
|
check_true "service has >= 1 species" '(.species | length) >= 1'
|
|
|
|
check "products page size 5" 200 GET "/products?size=5"
|
|
check_true "content length <= 5" '(.content | length) <= 5'
|
|
check_true "content length = min(5, total)" '(.content | length) == (if .totalElements < 5 then .totalElements else 5 end)'
|
|
|
|
echo "--- Pagination math ---"
|
|
check "pagination check" 200 GET "/products?page=0&size=10"
|
|
check_field "page number is 0" '.number' '0'
|
|
check_field "page size is 10" '.size' '10'
|
|
|
|
check "products default page" 200 GET "/products"
|
|
TOTAL=$(jq -r '.totalElements' /tmp/qa_body.json)
|
|
SIZE=$(jq -r '.size' /tmp/qa_body.json)
|
|
PAGES=$(jq -r '.totalPages' /tmp/qa_body.json)
|
|
EXPECTED_PAGES=$(( (TOTAL + SIZE - 1) / SIZE ))
|
|
if [ "$PAGES" = "$EXPECTED_PAGES" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: totalPages=$PAGES expected=$EXPECTED_PAGES (total=$TOTAL size=$SIZE)"; fi
|
|
|
|
check "activity logs limit 3" 200 GET "/activity-logs?limit=3" "${A[@]}"
|
|
check_true "logs length <= 3" 'length <= 3'
|
|
|
|
echo "--- Cart cardinality ---"
|
|
curl -s -o /dev/null -X DELETE "$BASE/cart" "${C[@]}" 2>/dev/null
|
|
curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' 2>/dev/null
|
|
check "cart after add 1" 200 GET "/cart?storeId=1" "${C[@]}"
|
|
CART_LEN1=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null)
|
|
|
|
curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":2,"storeId":1,"quantity":1}' 2>/dev/null
|
|
check "cart after add 2" 200 GET "/cart?storeId=1" "${C[@]}"
|
|
CART_LEN2=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null)
|
|
|
|
curl -s -o /dev/null -X POST "$BASE/cart/add" "${C[@]}" "${J[@]}" -d '{"prodId":1,"storeId":1,"quantity":1}' 2>/dev/null
|
|
check "cart after re-add 1" 200 GET "/cart?storeId=1" "${C[@]}"
|
|
CART_LEN3=$(jq '.items | length' /tmp/qa_body.json 2>/dev/null)
|
|
|
|
if [ "$CART_LEN2" = "2" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: cart should have 2 items after adding 2 products, got $CART_LEN2"; fi
|
|
if [ "$CART_LEN3" = "2" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: cart should still have 2 items (qty merged), got $CART_LEN3"; fi
|
|
|
|
curl -s -o /dev/null -X DELETE "$BASE/cart" "${C[@]}" 2>/dev/null
|
|
|
|
echo "--- Availability cardinality ---"
|
|
check "avail no dups" 200 GET "/appointments/availability?storeId=1&serviceId=1&date=2027-09-01" "${A[@]}"
|
|
check_true "no duplicate times" '[. | group_by(.) | all(length == 1)]'
|
|
|
|
###############################################################################
|
|
echo ""
|
|
echo "========== T — TIME =========="
|
|
###############################################################################
|
|
|
|
echo "--- Timestamp freshness ---"
|
|
NOW_EPOCH=$(date -u +%s)
|
|
check "create product" 201 POST "/products" "${A[@]}" "${J[@]}" -d '{"prodName":"QA_TIME_TEST","prodPrice":5.55,"categoryId":1}'
|
|
TPROD_ID=$(jq -r '.prodId // empty' /tmp/qa_body.json)
|
|
[ -n "$TPROD_ID" ] && CLEANUP_PRODUCT_IDS+=("$TPROD_ID")
|
|
|
|
if [ -n "$TPROD_ID" ]; then
|
|
check "get created product" 200 GET "/products/$TPROD_ID"
|
|
CREATED_AT=$(jq -r '.createdAt // empty' /tmp/qa_body.json)
|
|
if [ -n "$CREATED_AT" ]; then
|
|
CREATED_EPOCH=$(date -u -d "$CREATED_AT" +%s 2>/dev/null || echo "0")
|
|
DIFF=$((NOW_EPOCH - CREATED_EPOCH))
|
|
if [ "$DIFF" -ge "-10" ] && [ "$DIFF" -le "120" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: createdAt not recent (diff=${DIFF}s)"; fi
|
|
else
|
|
FAIL=$((FAIL+1)); echo "FAIL: createdAt is null"
|
|
fi
|
|
|
|
check "update product" 200 PUT "/products/$TPROD_ID" "${A[@]}" "${J[@]}" -d "{\"prodName\":\"QA_TIME_UPD\",\"prodPrice\":6.66,\"categoryId\":1}"
|
|
check "get updated product" 200 GET "/products/$TPROD_ID"
|
|
check_true "updatedAt >= createdAt" '(.updatedAt // .createdAt) >= .createdAt'
|
|
fi
|
|
|
|
check "create sale" 201 POST "/sales" "${S[@]}" "${J[@]}" -d '{"storeId":1,"paymentMethod":"Cash","channel":"IN_STORE","items":[{"prodId":2,"quantity":1}]}'
|
|
TSALE_ID=$(jq -r '.saleId // empty' /tmp/qa_body.json)
|
|
[ -n "$TSALE_ID" ] && CLEANUP_SALE_IDS+=("$TSALE_ID")
|
|
if [ -n "$TSALE_ID" ]; then
|
|
check "get sale" 200 GET "/sales/$TSALE_ID" "${A[@]}"
|
|
check_true "saleDate has T (recent)" '.saleDate | test("T")'
|
|
fi
|
|
|
|
check "conversations for chat" 200 GET "/chat/conversations" "${C[@]}"
|
|
CHAT_CONV=$(jq -r '.[0].conversationId // empty' /tmp/qa_body.json)
|
|
if [ -n "$CHAT_CONV" ]; then
|
|
curl -s -o /dev/null -X POST "$BASE/chat/conversations/$CHAT_CONV/messages" "${C[@]}" "${J[@]}" -d '{"content":"QA time test message"}' 2>/dev/null
|
|
check "get chat messages" 200 GET "/chat/conversations/$CHAT_CONV/messages" "${C[@]}"
|
|
LAST_TS=$(jq -r '.[-1].timestamp // empty' /tmp/qa_body.json)
|
|
if [ -n "$LAST_TS" ]; then
|
|
MSG_EPOCH=$(date -u -d "$LAST_TS" +%s 2>/dev/null || echo "0")
|
|
MDIFF=$((NOW_EPOCH - MSG_EPOCH))
|
|
if [ "$MDIFF" -ge "-10" ] && [ "$MDIFF" -le "300" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: chat message timestamp not recent (diff=${MDIFF}s)"; fi
|
|
fi
|
|
fi
|
|
|
|
echo "--- Temporal constraints ---"
|
|
TODAY=$(date -u +%Y-%m-%d)
|
|
check "scheduled appts" 200 GET "/appointments?status=Scheduled&size=50" "${A[@]}"
|
|
check_true "scheduled dates >= today" "[.content[].appointmentDate] | all(. >= \"$TODAY\")"
|
|
|
|
check "coupons temporal" 200 GET "/coupons" "${A[@]}"
|
|
NOW_ISO=$(date -u +%Y-%m-%dT%H:%M:%S)
|
|
check_true "active coupons endsAt valid" "[.[] | select(.endsAt != null) | .endsAt] | all(. >= \"$NOW_ISO\" or . == null)"
|
|
|
|
echo "--- Sequencing ---"
|
|
if [ -n "${CHAT_CONV:-}" ]; then
|
|
check "chat sequence" 200 GET "/chat/conversations/$CHAT_CONV/messages" "${C[@]}"
|
|
check_true "chat timestamps non-decreasing" '. as $a | [range(1;length)] | all(. as $i | $a[$i].timestamp >= $a[$i-1].timestamp)'
|
|
fi
|
|
|
|
check "activity log sequence" 200 GET "/activity-logs?limit=20" "${A[@]}"
|
|
check_true "logs non-increasing" '. as $a | [range(1;length)] | all(. as $i | $a[$i].logTimestamp <= $a[$i-1].logTimestamp)'
|
|
|
|
echo "--- Token timing ---"
|
|
FRESH_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
|
|
check "fresh token works" 200 GET "/auth/me" -H "Authorization: Bearer $FRESH_TOKEN"
|
|
|
|
curl -s -o /dev/null -X POST "$BASE/auth/logout" -H "Authorization: Bearer $FRESH_TOKEN" 2>/dev/null
|
|
LOGOUT_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X GET "$BASE/auth/me" -H "Authorization: Bearer $FRESH_TOKEN" 2>/dev/null)
|
|
if [ "$LOGOUT_CODE" = "401" ]; then PASS=$((PASS+1))
|
|
else FAIL=$((FAIL+1)); echo "FAIL: old token after logout — expected 401 got $LOGOUT_CODE"; fi
|
|
|
|
ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
|
|
A=(-H "Authorization: Bearer $ADMIN_TOKEN")
|
|
|
|
echo ""
|
|
echo "========================================="
|
|
echo "CORRECT RESULTS: $PASS passed, $FAIL failed"
|
|
echo "========================================="
|