#!/bin/bash # 2026-04-09 — Полный набор тестов SQS API для ручного / автоматического запуска # Покрывает: happy path, невалидный ввод, edge cases, безопасность # Запуск: bash sqs_api_tests.sh # Требования: curl, base64, доступ к https://sqs.kube5s.ru set -euo pipefail # ═══════════════════════════════════════════ # КОНФИГУРАЦИЯ # ═══════════════════════════════════════════ SQS_HOST="https://sqs.kube5s.ru" SQS_BASE="${SQS_HOST}/sqs/test001" AK="SQSAK-test001-ae4dc8" SK="J8mElXq8Nwww8iFRtGJCm8KfxM9XSTluTqS2A67xsEE" AUTH="${AK}:${SK}" VERSION="2012-11-05" # Счётчики результатов PASS=0 FAIL=0 SKIP=0 TOTAL=0 # Уникальный суффикс чтобы тесты не конфликтовали при повторном запуске TS=$(date +%s) # ═══════════════════════════════════════════ # УТИЛИТЫ # ═══════════════════════════════════════════ sqs_call() { # $1 = параметры запроса (после ?) # $2 = auth (опционально, по умолчанию $AUTH) local auth="${2:-$AUTH}" curl -s --max-time 15 -u "$auth" "${SQS_BASE}?${1}&Version=${VERSION}" 2>&1 } assert_contains() { # $1 = название теста # $2 = ответ # $3 = ожидаемая подстрока TOTAL=$((TOTAL + 1)) if echo "$2" | grep -q "$3"; then PASS=$((PASS + 1)) echo " ✅ PASS: $1" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: $1" echo " Ожидалось: '$3'" echo " Получено: $(echo "$2" | head -3)" fi } assert_not_contains() { # $1 = название теста # $2 = ответ # $3 = подстрока которой НЕ должно быть TOTAL=$((TOTAL + 1)) if echo "$2" | grep -q "$3"; then FAIL=$((FAIL + 1)) echo " ❌ FAIL: $1" echo " НЕ ожидалось: '$3'" echo " Получено: $(echo "$2" | head -3)" else PASS=$((PASS + 1)) echo " ✅ PASS: $1" fi } assert_http_code() { # $1 = название теста # $2 = URL (полный) # $3 = ожидаемый HTTP код # $4 = auth (опционально) TOTAL=$((TOTAL + 1)) local auth="${4:-$AUTH}" local code code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 15 -u "$auth" "$2" 2>&1) if [ "$code" = "$3" ]; then PASS=$((PASS + 1)) echo " ✅ PASS: $1 (HTTP $code)" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: $1 (ожидался HTTP $3, получен $code)" fi } cleanup_queue() { # Тихое удаление очереди sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/$1" >/dev/null 2>&1 || true } # ═══════════════════════════════════════════ echo "╔══════════════════════════════════════════════════╗" echo "║ SQS API Test Suite — $(date '+%Y-%m-%d %H:%M:%S') ║" echo "║ Endpoint: ${SQS_BASE} ║" echo "╚══════════════════════════════════════════════════╝" echo "" # ═══════════════════════════════════════════ # ГРУППА 1: БАЗОВЫЕ ОПЕРАЦИИ (Happy Path) # ═══════════════════════════════════════════ echo "── ГРУППА 1: Базовые операции (Happy Path) ──" Q1="testq-hp-${TS}" # T01: Создание очереди R=$(sqs_call "Action=CreateQueue&QueueName=${Q1}") assert_contains "T01 CreateQueue" "$R" "CreateQueueResult" assert_contains "T01 CreateQueue URL" "$R" "${Q1}" # T02: Повторное создание той же очереди — не ошибка, возвращает ту же URL R=$(sqs_call "Action=CreateQueue&QueueName=${Q1}") assert_contains "T02 CreateQueue idempotent" "$R" "CreateQueueResult" # T03: ListQueues — очередь видна R=$(sqs_call "Action=ListQueues") assert_contains "T03 ListQueues" "$R" "${Q1}" # T04: SendMessage R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=Hello") assert_contains "T04 SendMessage" "$R" "MessageId" assert_contains "T04 SendMessage MD5" "$R" "MD5OfMessageBody" # T05: ReceiveMessage R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}") assert_contains "T05 ReceiveMessage Body" "$R" "Hello" assert_contains "T05 ReceiveMessage ReceiptHandle" "$R" "ReceiptHandle" # Извлекаем ReceiptHandle для T06 RH=$(echo "$R" | grep -oP '(?<=)[^<]+' | head -1) # T06: DeleteMessage # ReceiptHandle содержит +/= — URL-кодируем чтобы не сломать query string if [ -n "$RH" ]; then RH_ENC=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$RH") R=$(sqs_call "Action=DeleteMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&ReceiptHandle=${RH_ENC}") assert_contains "T06 DeleteMessage" "$R" "DeleteMessageResponse" else TOTAL=$((TOTAL + 1)); SKIP=$((SKIP + 1)) echo " ⚠️ SKIP: T06 DeleteMessage — не удалось получить ReceiptHandle" fi # T07: ReceiveMessage из пустой очереди — пустой результат R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q1}") assert_not_contains "T07 ReceiveMessage empty queue" "$R" "" # T08: GetQueueAttributes R=$(sqs_call "Action=GetQueueAttributes&QueueUrl=${SQS_BASE}/test001/${Q1}&AttributeName.1=All") assert_contains "T08 GetQueueAttributes" "$R" "GetQueueAttributesResponse" # T09: PurgeQueue sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q1}&MessageBody=ToPurge" >/dev/null R=$(sqs_call "Action=PurgeQueue&QueueUrl=${SQS_BASE}/test001/${Q1}") assert_contains "T09 PurgeQueue" "$R" "PurgeQueueResponse" # T10: DeleteQueue R=$(sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/${Q1}") assert_contains "T10 DeleteQueue" "$R" "DeleteQueueResponse" # T11: ListQueues — очередь исчезла R=$(sqs_call "Action=ListQueues") assert_not_contains "T11 ListQueues after delete" "$R" "${Q1}" echo "" # ═══════════════════════════════════════════ # ГРУППА 2: НЕВАЛИДНЫЙ ВВОД # ═══════════════════════════════════════════ echo "── ГРУППА 2: Невалидный ввод ──" # T12: Неизвестный Action R=$(sqs_call "Action=BogusAction") assert_contains "T12 Unknown Action" "$R" "InvalidAction\|AWS.SimpleQueueService.NonExistentAction\|UnknownAction\|InvalidParameterValue" # T13: Пустой Action R=$(sqs_call "Action=") assert_contains "T13 Empty Action" "$R" "Error\|MissingAction\|InvalidAction\|Value for parameter Action is not valid" # T14: Без Action вообще R=$(curl -s --max-time 15 -u "$AUTH" "${SQS_BASE}?Version=${VERSION}" 2>&1) assert_contains "T14 Missing Action" "$R" "Error\|MissingAction\|request" # T15: CreateQueue без QueueName R=$(sqs_call "Action=CreateQueue") assert_contains "T15 CreateQueue no name" "$R" "Error\|MissingParameter\|Value for parameter QueueName is not valid" # T16: CreateQueue с пустым QueueName R=$(sqs_call "Action=CreateQueue&QueueName=") assert_contains "T16 CreateQueue empty name" "$R" "Error\|InvalidParameterValue" # T17: CreateQueue с недопустимыми символами (пробелы, кириллица) # Akka HTTP (ElasticMQ) отвечает "Illegal request-target" на raw кириллицу R=$(sqs_call "Action=CreateQueue&QueueName=очередь%20тест") assert_contains "T17 CreateQueue invalid chars" "$R" "Error\|InvalidParameterValue\|Illegal" # T18: CreateQueue с очень длинным именем (>80 символов) LONGNAME=$(printf 'q%.0s' {1..100}) R=$(sqs_call "Action=CreateQueue&QueueName=${LONGNAME}") assert_contains "T18 CreateQueue name too long" "$R" "Error\|InvalidParameterValue\|CreateQueueResult" # Примечание: ElasticMQ может создать — это допустимо если не strict AWS # T19: SendMessage без MessageBody R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/nonexist") assert_contains "T19 SendMessage no body" "$R" "Error\|MissingParameter\|The request must contain the parameter MessageBody" # T20: SendMessage без QueueUrl R=$(sqs_call "Action=SendMessage&MessageBody=test") assert_contains "T20 SendMessage no QueueUrl" "$R" "Error\|MissingParameter\|AWS.SimpleQueueService.NonExistentQueue\|The request must contain" echo "" # ═══════════════════════════════════════════ # ГРУППА 3: НЕСУЩЕСТВУЮЩИЕ РЕСУРСЫ # ═══════════════════════════════════════════ echo "── ГРУППА 3: Несуществующие ресурсы ──" # T21: SendMessage в несуществующую очередь (autoCreateQueues=false!) R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}&MessageBody=fail") assert_contains "T21 SendMessage nonexistent queue" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" # T22: ReceiveMessage из несуществующей очереди R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}") assert_contains "T22 ReceiveMessage nonexistent queue" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" # T23: DeleteQueue несуществующей очереди R=$(sqs_call "Action=DeleteQueue&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}") assert_contains "T23 DeleteQueue nonexistent" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" # T24: GetQueueAttributes несуществующей очереди R=$(sqs_call "Action=GetQueueAttributes&QueueUrl=${SQS_BASE}/test001/nonexist-${TS}&AttributeName.1=All") assert_contains "T24 GetQueueAttributes nonexistent" "$R" "NonExistentQueue\|AWS.SimpleQueueService.NonExistentQueue\|does not exist\|Error" # T25: DeleteMessage с фейковым ReceiptHandle Q25="testq-rh-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q25}" >/dev/null R=$(sqs_call "Action=DeleteMessage&QueueUrl=${SQS_BASE}/test001/${Q25}&ReceiptHandle=fake-receipt-handle-12345") assert_contains "T25 DeleteMessage fake receipt" "$R" "Error\|ReceiptHandleIsInvalid\|DeleteMessageResponse" # Примечание: ElasticMQ может молча игнорировать — это допустимо cleanup_queue "$Q25" echo "" # ═══════════════════════════════════════════ # ГРУППА 4: АУТЕНТИФИКАЦИЯ И БЕЗОПАСНОСТЬ # ═══════════════════════════════════════════ echo "── ГРУППА 4: Аутентификация и безопасность ──" # T26: Неправильный Access Key R=$(sqs_call "Action=ListQueues" "WRONG-KEY:${SK}") assert_contains "T26 Wrong access key" "$R" "InvalidClientTokenId\|SignatureDoesNotMatch\|Error\|403\|401\|Unauthorized" # T27: Неправильный Secret Key R=$(sqs_call "Action=ListQueues" "${AK}:WRONG-SECRET") assert_contains "T27 Wrong secret key" "$R" "SignatureDoesNotMatch\|Error\|403\|401\|Unauthorized" # T28: Пустые credentials R=$(curl -s --max-time 15 "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" 2>&1) assert_contains "T28 No credentials" "$R" "Error\|AuthFailure\|MissingAuthenticationToken\|ListQueuesResult\|403\|401" # Примечание: ElasticMQ может не требовать auth — если так, это проблема безопасности # T29: SQL injection в QueueName R=$(sqs_call "Action=CreateQueue&QueueName=test';DROP%20TABLE%20queues;--") assert_contains "T29 SQL injection in QueueName" "$R" "Error\|InvalidParameterValue\|CreateQueueResult" # Если создалась — OK, ElasticMQ in-memory, нет SQL. Убираем. cleanup_queue "test';DROP TABLE queues;--" # T30: XSS в MessageBody Q30="testq-xss-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q30}" >/dev/null R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q30}&MessageBody=%3Cscript%3Ealert(1)%3C/script%3E") assert_contains "T30 XSS in MessageBody — accepted" "$R" "MessageId" # SQS должен принять любой body — это не HTML, а очередь. Проверяем что возвращает как есть: R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q30}") assert_contains "T30 XSS body returned" "$R" "script\|<script" cleanup_queue "$Q30" echo "" # ═══════════════════════════════════════════ # ГРУППА 5: СПЕЦСИМВОЛЫ И КОДИРОВКИ # ═══════════════════════════════════════════ echo "── ГРУППА 5: Спецсимволы и кодировки ──" Q5="testq-enc-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q5}" >/dev/null # T31: Unicode в MessageBody R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82") assert_contains "T31 Unicode body" "$R" "MessageId" # T32: Receive Unicode R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q5}") assert_contains "T32 Receive Unicode" "$R" "Привет\|%D0%9F" # T33: Амперсанд и XML-спецсимволы в body R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=a%26b%3Cc%3Ed") assert_contains "T33 XML special chars" "$R" "MessageId" # T34: JSON в body R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=%7B%22key%22%3A%22value%22%7D") assert_contains "T34 JSON body" "$R" "MessageId" # T35: Пустой MessageBody R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q5}&MessageBody=") assert_contains "T35 Empty body" "$R" "Error\|MissingParameter\|MessageId" cleanup_queue "$Q5" echo "" # ═══════════════════════════════════════════ # ГРУППА 6: НАГРУЗКА И ПОРЯДОК # ═══════════════════════════════════════════ echo "── ГРУППА 6: Множественные сообщения ──" Q6="testq-multi-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q6}" >/dev/null # T36: Отправить 10 сообщений подряд ALL_OK=true for i in $(seq 1 10); do R=$(sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&MessageBody=msg-${i}") if ! echo "$R" | grep -q "MessageId"; then ALL_OK=false break fi done TOTAL=$((TOTAL + 1)) if $ALL_OK; then PASS=$((PASS + 1)) echo " ✅ PASS: T36 Send 10 messages" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: T36 Send 10 messages" fi # T37: Receive все 10 (может потребовать несколько вызовов) RECEIVED=0 for _ in $(seq 1 15); do R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q6}&MaxNumberOfMessages=10") COUNT=$(echo "$R" | grep -c "" || true) RECEIVED=$((RECEIVED + COUNT)) if [ "$RECEIVED" -ge 10 ]; then break; fi done TOTAL=$((TOTAL + 1)) if [ "$RECEIVED" -ge 10 ]; then PASS=$((PASS + 1)) echo " ✅ PASS: T37 Receive all 10 messages (got ${RECEIVED})" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: T37 Receive all 10 messages (got ${RECEIVED}/10)" fi cleanup_queue "$Q6" echo "" # ═══════════════════════════════════════════ # ГРУППА 7: HTTP-УРОВЕНЬ # ═══════════════════════════════════════════ echo "── ГРУППА 7: HTTP-уровень ──" # T38: Проверка HTTPS assert_http_code "T38 HTTPS endpoint" "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" "200" # T39: Health endpoint TOTAL=$((TOTAL + 1)) HC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "${SQS_BASE%/sqs/test001}/sqs/test001/health" 2>&1 || echo "000") # Примечание: health может быть на другом пути if [ "$HC" = "200" ] || [ "$HC" = "404" ]; then PASS=$((PASS + 1)) echo " ✅ PASS: T39 Health endpoint (HTTP $HC)" else SKIP=$((SKIP + 1)) echo " ⚠️ SKIP: T39 Health endpoint (HTTP $HC — может быть не на этом пути)" fi # T40: Очень большой запрос (64KB body) Q40="testq-big-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q40}" >/dev/null BIGBODY=$(python3 -c "print('A'*65000)" 2>/dev/null || printf 'A%.0s' $(seq 1 65000)) R=$(curl -s --max-time 15 -u "$AUTH" -X POST "${SQS_BASE}?Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q40}&Version=${VERSION}" --data-urlencode "MessageBody=${BIGBODY}" 2>&1) assert_contains "T40 Large body 64KB" "$R" "MessageId\|Error\|TooLong\|InvalidParameterValue" cleanup_queue "$Q40" # T41: POST вместо GET R=$(curl -s --max-time 15 -u "$AUTH" -X POST "${SQS_BASE}?Action=ListQueues&Version=${VERSION}" 2>&1) assert_contains "T41 POST method" "$R" "ListQueuesResult\|ListQueuesResponse" # T42: Несуществующий путь TOTAL=$((TOTAL + 1)) HC=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 -u "$AUTH" "${SQS_HOST}/sqs/nonexistent-tenant?Action=ListQueues&Version=${VERSION}" 2>&1) if [ "$HC" = "404" ] || [ "$HC" = "403" ] || [ "$HC" = "502" ] || [ "$HC" = "503" ] || [ "$HC" = "302" ]; then PASS=$((PASS + 1)) echo " ✅ PASS: T42 Wrong tenant path (HTTP $HC)" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: T42 Wrong tenant path (HTTP $HC — ожидался 302/404/403)" fi echo "" # ═══════════════════════════════════════════ # ГРУППА 8: ТАЙМАУТЫ И LONG POLLING # ═══════════════════════════════════════════ echo "── ГРУППА 8: Long polling и таймауты ──" Q8="testq-poll-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q8}" >/dev/null # T43: WaitTimeSeconds=1 на пустой очереди — должен вернуть пустой через ~1 сек TOTAL=$((TOTAL + 1)) T_START=$(date +%s) R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q8}&WaitTimeSeconds=1") T_END=$(date +%s) ELAPSED=$((T_END - T_START)) if [ "$ELAPSED" -ge 1 ] && [ "$ELAPSED" -le 5 ]; then PASS=$((PASS + 1)) echo " ✅ PASS: T43 Long poll 1s empty queue (${ELAPSED}s)" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: T43 Long poll 1s (${ELAPSED}s — ожидалось 1-5s)" fi # T44: WaitTimeSeconds=0 — мгновенный ответ TOTAL=$((TOTAL + 1)) T_START=$(date +%s) R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q8}&WaitTimeSeconds=0") T_END=$(date +%s) ELAPSED=$((T_END - T_START)) if [ "$ELAPSED" -le 2 ]; then PASS=$((PASS + 1)) echo " ✅ PASS: T44 Short poll instant response (${ELAPSED}s)" else FAIL=$((FAIL + 1)) echo " ❌ FAIL: T44 Short poll (${ELAPSED}s — ожидалось <2s)" fi cleanup_queue "$Q8" echo "" # ═══════════════════════════════════════════ # ГРУППА 9: VISIBILITY TIMEOUT # ═══════════════════════════════════════════ echo "── ГРУППА 9: Visibility timeout ──" Q9="testq-vis-${TS}" sqs_call "Action=CreateQueue&QueueName=${Q9}" >/dev/null sqs_call "Action=SendMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&MessageBody=VisTest" >/dev/null # T45: Receive с VisibilityTimeout=2 — сообщение скрыто, повторный receive пуст R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&VisibilityTimeout=2") assert_contains "T45a Receive with visibility" "$R" "VisTest" R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&WaitTimeSeconds=0") assert_not_contains "T45b Hidden during visibility" "$R" "VisTest" # T46: После visibility timeout — сообщение снова доступно sleep 3 R=$(sqs_call "Action=ReceiveMessage&QueueUrl=${SQS_BASE}/test001/${Q9}&WaitTimeSeconds=0") assert_contains "T46 Re-visible after timeout" "$R" "VisTest" cleanup_queue "$Q9" echo "" # ═══════════════════════════════════════════ # РЕЗУЛЬТАТЫ # ═══════════════════════════════════════════ echo "╔══════════════════════════════════════════════════╗" echo "║ РЕЗУЛЬТАТЫ ║" echo "╠══════════════════════════════════════════════════╣" echo "║ Всего: ${TOTAL} ║" echo "║ ✅ PASS: ${PASS} ║" echo "║ ❌ FAIL: ${FAIL} ║" echo "║ ⚠️ SKIP: ${SKIP} ║" echo "╚══════════════════════════════════════════════════╝" if [ "$FAIL" -gt 0 ]; then exit 1 else exit 0 fi