From 38194fb2fa9300dd06f4a50a92dd706d22675a89 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 11 Apr 2026 09:44:38 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20v0.1.18=20=E2=80=94=20TagQueue/UntagQue?= =?UTF-8?q?ue/ListQueueTags/ChangeMessageVisibilityBatch,=20JWT=20auth=20i?= =?UTF-8?q?n=20UI=20API=20tests,=20hardcore=5Ftest=20JWT=20fix,=20Helm=20c?= =?UTF-8?q?hart=20v0.1.18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/models.go | 1 - app/router/router.go | 32 +++---- tests/hardcore_test.sh | 212 +++++++++++++++++++++++++++++------------ tests/quick_test.sh | 205 ++++++++++++++++++++++++++++----------- tests/tag_debug_tmp.sh | 26 +++++ 5 files changed, 340 insertions(+), 136 deletions(-) create mode 100644 tests/tag_debug_tmp.sh diff --git a/app/models/models.go b/app/models/models.go index 366813b..5529afe 100644 --- a/app/models/models.go +++ b/app/models/models.go @@ -18,7 +18,6 @@ type MessageAttribute struct { StringValue string `json:"StringValue,omitempty" xml:"StringValue,omitempty"` } - type SqsMessage struct { MessageBody string Uuid string diff --git a/app/router/router.go b/app/router/router.go index 5983c6b..ebcd144 100644 --- a/app/router/router.go +++ b/app/router/router.go @@ -97,23 +97,23 @@ func encodeResponse(w http.ResponseWriter, req *http.Request, statusCode int, bo // routingTableV1 — только SQS actions (SNS удалён) var routingTableV1 = map[string]func(r *http.Request) (int, interfaces.AbstractResponseBody){ - "CreateQueue": sqs.CreateQueueV1, - "ListQueues": sqs.ListQueuesV1, - "GetQueueAttributes": sqs.GetQueueAttributesV1, - "SetQueueAttributes": sqs.SetQueueAttributesV1, - "SendMessage": sqs.SendMessageV1, - "ReceiveMessage": sqs.ReceiveMessageV1, - "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, - "DeleteMessage": sqs.DeleteMessageV1, - "GetQueueUrl": sqs.GetQueueUrlV1, - "PurgeQueue": sqs.PurgeQueueV1, - "DeleteQueue": sqs.DeleteQueueV1, - "SendMessageBatch": sqs.SendMessageBatchV1, - "DeleteMessageBatch": sqs.DeleteMessageBatchV1, + "CreateQueue": sqs.CreateQueueV1, + "ListQueues": sqs.ListQueuesV1, + "GetQueueAttributes": sqs.GetQueueAttributesV1, + "SetQueueAttributes": sqs.SetQueueAttributesV1, + "SendMessage": sqs.SendMessageV1, + "ReceiveMessage": sqs.ReceiveMessageV1, + "ChangeMessageVisibility": sqs.ChangeMessageVisibilityV1, + "DeleteMessage": sqs.DeleteMessageV1, + "GetQueueUrl": sqs.GetQueueUrlV1, + "PurgeQueue": sqs.PurgeQueueV1, + "DeleteQueue": sqs.DeleteQueueV1, + "SendMessageBatch": sqs.SendMessageBatchV1, + "DeleteMessageBatch": sqs.DeleteMessageBatchV1, "ChangeMessageVisibilityBatch": sqs.ChangeMessageVisibilityBatchV1, - "TagQueue": sqs.TagQueueV1, - "UntagQueue": sqs.UntagQueueV1, - "ListQueueTags": sqs.ListQueueTagsV1, + "TagQueue": sqs.TagQueueV1, + "UntagQueue": sqs.UntagQueueV1, + "ListQueueTags": sqs.ListQueueTagsV1, } func health(w http.ResponseWriter, req *http.Request) { diff --git a/tests/hardcore_test.sh b/tests/hardcore_test.sh index f832dff..200a12f 100644 --- a/tests/hardcore_test.sh +++ b/tests/hardcore_test.sh @@ -1,10 +1,10 @@ #!/bin/bash # hardcore_test.sh — расширенный тест shared-sqs -# Изменено: 2026-04-09 -# Покрывает: Admin API, AWS CLI CRUD, awscurl CRUD, кросс-доставка, -# изоляция тенантов, невалидный ввод, спецсимволы, -# visibility timeout, batch, лимиты очередей, cleanup тенанта, -# UI API (create/delete queue, send/peek/purge messages, изоляция). +# Created: 2026-04-09 +# Updated: 2026-04-11 — добавлены TagQueue/UntagQueue/ListQueueTags/ChangeMessageVisibilityBatch, +# UI API с JWT авторизацией (nubes api_token). +# Покрывает: все 17 SQS команд, Admin API, AWS CLI + awscurl, кросс-доставка, +# изоляция тенантов, невалидный ввод, batch, лимиты, UI API с JWT. # Запуск: BASE_URL=https://qu.kube5s.ru ADMIN_TOKEN=... bash tests/hardcore_test.sh set -uo pipefail @@ -15,6 +15,8 @@ set -uo pipefail BASE_URL="${BASE_URL:-https://qu.kube5s.ru}" ADMIN_TOKEN="${ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" AWSCURL="${AWSCURL:-/home/naeel/.local/bin/awscurl}" +# JWT-токен для авторизации в UI API (/ui/api/*); получается через nubes.ru → api_token +NUBES_JWT="${NUBES_JWT:-eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg}" TS=$(date +%s) PASS=0 @@ -77,6 +79,21 @@ sqs() { aws --endpoint-url "$BASE_URL" --output json sqs "$@" 2>&1 } +# ui_jwt — UI API запрос с JWT авторизацией +ui_jwt() { + local method="$1" path="$2" body="${3:-}" + if [[ -n "$body" ]]; then + curl -s --max-time 15 -X "$method" \ + -H "Authorization: Bearer $NUBES_JWT" \ + -H "Content-Type: application/json" \ + -d "$body" "${BASE_URL}/ui/api${path}" 2>&1 + else + curl -s --max-time 15 -X "$method" \ + -H "Authorization: Bearer $NUBES_JWT" \ + "${BASE_URL}/ui/api${path}" 2>&1 + fi +} + # acurl — awscurl враппер для SQS Query Protocol # $1=ak, $2=sk, остальное — form-encoded body (Action=...&...) acurl() { @@ -516,6 +533,85 @@ sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$BQURL" >/dev/null 2>&1 || true echo "" +# ═══════════════════════════════════════════ +echo "── 10б. TagQueue / UntagQueue / ListQueueTags ──" +# ═══════════════════════════════════════════ + +TAG_Q="hc-tag-${TS}" +TAG_QURL=$(qurl "$T1_ID" "$TAG_Q") +sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$TAG_Q" >/dev/null 2>&1 || true + +# TagQueue — добавить теги +R=$(sqs "$T1_AK" "$T1_SK" tag-queue --queue-url "$TAG_QURL" --tags env=prod,team=backend 2>&1 && echo "OK" || echo "FAIL") +check "TagQueue: добавить теги → OK" "$R" "OK" + +# ListQueueTags — теги видны +R=$(sqs "$T1_AK" "$T1_SK" list-queue-tags --queue-url "$TAG_QURL") +check "ListQueueTags: env=prod видна" "$R" "env|prod" +check "ListQueueTags: team=backend видна" "$R" "team|backend" + +# TagQueue — обновить существующий тег (merge) +R=$(sqs "$T1_AK" "$T1_SK" tag-queue --queue-url "$TAG_QURL" --tags env=staging 2>&1 && echo "OK" || echo "FAIL") +check "TagQueue: обновить тег env → OK" "$R" "OK" + +R=$(sqs "$T1_AK" "$T1_SK" list-queue-tags --queue-url "$TAG_QURL") +check "ListQueueTags: env обновлён до staging" "$R" "staging" +check "ListQueueTags: team не удалён при обновлении" "$R" "backend" + +# UntagQueue — удалить один тег +R=$(sqs "$T1_AK" "$T1_SK" untag-queue --queue-url "$TAG_QURL" --tag-keys env 2>&1 && echo "OK" || echo "FAIL") +check "UntagQueue: удалить тег env → OK" "$R" "OK" + +R=$(sqs "$T1_AK" "$T1_SK" list-queue-tags --queue-url "$TAG_QURL") +check_not "ListQueueTags: env удалён" "$R" '"env"' +check "ListQueueTags: team остался" "$R" "backend" + +sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$TAG_QURL" >/dev/null 2>&1 || true + +echo "" + +# ═══════════════════════════════════════════ +echo "── 10в. ChangeMessageVisibilityBatch ──" +# ═══════════════════════════════════════════ + +CMVB_Q="hc-cmvb-${TS}" +CMVB_QURL=$(qurl "$T1_ID" "$CMVB_Q") +sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$CMVB_Q" >/dev/null 2>&1 || true + +# Отправляем 3 сообщения +for i in 1 2 3; do + sqs "$T1_AK" "$T1_SK" send-message --queue-url "$CMVB_QURL" --message-body "cmvb-msg-${i}" >/dev/null 2>&1 || true +done + +# Получаем все 3 (visibility=60 чтобы не конкурировали) +R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$CMVB_QURL" --max-number-of-messages 3 --visibility-timeout 60) +CMVB_RECEIPTS=$(echo "$R" | jq -r '.Messages[]?.ReceiptHandle' 2>/dev/null || true) + +if [[ -n "$CMVB_RECEIPTS" ]]; then + CMVB_ENTRIES=$(echo "$CMVB_RECEIPTS" | python3 -c " +import sys +entries = [] +for i, rh in enumerate(sys.stdin.read().strip().split('\n'), 1): + if rh: + entries.append('{\"Id\":\"e%d\",\"ReceiptHandle\":\"%s\",\"VisibilityTimeout\":120}' % (i, rh)) +print('[' + ','.join(entries) + ']') +" 2>/dev/null || echo "[]") + if [[ "$CMVB_ENTRIES" != "[]" ]]; then + R=$(sqs "$T1_AK" "$T1_SK" change-message-visibility-batch \ + --queue-url "$CMVB_QURL" \ + --entries "$CMVB_ENTRIES") + check "ChangeMessageVisibilityBatch → Successful" "$R" "Successful" + check_not "ChangeMessageVisibilityBatch → нет Failed" "$R" '"Failed"' + fi +else + TOTAL=$((TOTAL + 1)); FAIL=$((FAIL + 1)) + echo " ❌ ChangeMessageVisibilityBatch (нет сообщений для теста)" +fi + +sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$CMVB_QURL" >/dev/null 2>&1 || true + +echo "" + # ═══════════════════════════════════════════ echo "── 11. Лимит очередей (max_queues=2) ──" # ═══════════════════════════════════════════ @@ -900,28 +996,37 @@ sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$ENC_QURL" >/dev/null 2>&1 || tr echo "" # ═══════════════════════════════════════════ -echo "── 24. UI API — создание очереди через /ui/api ──" +echo "── 24. UI API — авторизация через JWT ──" # ═══════════════════════════════════════════ -UI_T=$(admin_api POST /admin/tenants '{"name":"ui-test-'"$TS"'","max_queues":10}') -UI_TID=$(echo "$UI_T" | jq -r '.id') -UI_AK=$(echo "$UI_T" | jq -r '.access_key') -UI_SK=$(echo "$UI_T" | jq -r '.secret_key') - -# Создать очередь через UI API -R=$(curl -s --max-time 15 -X POST "${BASE_URL}/ui/api/tenants/${UI_TID}/queues" \ +# Авторизуемся через nubes JWT +UI_AUTH=$(curl -s --max-time 15 -X POST \ -H "Content-Type: application/json" \ - -d '{"name":"ui-queue-1"}') + -d "{\"token\":\"$NUBES_JWT\"}" \ + "${BASE_URL}/ui/api/auth" 2>&1) +check "UI API: POST /auth → tenant_id" "$UI_AUTH" "tenant_id" +UI_TID=$(echo "$UI_AUTH" | jq -r '.tenant_id' 2>/dev/null || true) +UI_AK=$(echo "$UI_AUTH" | jq -r '.access_key' 2>/dev/null || true) +UI_SK=$(echo "$UI_AUTH" | jq -r '.secret_key' 2>/dev/null || true) + +echo " UI Tenant ID: $UI_TID" + +echo "" + +# ═══════════════════════════════════════════ +echo "── 25б. UI API — создание очереди ──" +# ═══════════════════════════════════════════ + +# Создать очередь через UI API с JWT +R=$(ui_jwt POST "/tenants/${UI_TID}/queues" '{"name":"ui-queue-1"}') check "UI API: создать очередь ui-queue-1 → имя" "$R" "ui-queue-1" # Дубль — та же очередь, не ошибка (идемпотентно) -R=$(curl -s --max-time 15 -X POST "${BASE_URL}/ui/api/tenants/${UI_TID}/queues" \ - -H "Content-Type: application/json" \ - -d '{"name":"ui-queue-1"}') +R=$(ui_jwt POST "/tenants/${UI_TID}/queues" '{"name":"ui-queue-1"}') check "UI API: повторное создание — idempotent (нет 500)" "$R" "ui-queue-1|error" # Список очередей через /ui/api содержит созданную -R=$(curl -s --max-time 15 "${BASE_URL}/ui/api/tenants/${UI_TID}/queues") +R=$(ui_jwt GET "/tenants/${UI_TID}/queues") check "UI API: GET /queues содержит ui-queue-1" "$R" "ui-queue-1" echo "" @@ -931,27 +1036,20 @@ echo "── 25. UI API — отправка и peek сообщений ──" # ═══════════════════════════════════════════ # Отправить сообщение через UI -R=$(curl -s --max-time 15 -X POST \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages" \ - -H "Content-Type: application/json" \ - -d '{"body":"hello-from-ui"}') +R=$(ui_jwt POST "/tenants/${UI_TID}/queues/ui-queue-1/messages" '{"body":"hello-from-ui"}') check "UI API: send message → id" "$R" '"id"' check "UI API: send message → status sent" "$R" "sent" # Отправить ещё одно -curl -s --max-time 15 -X POST \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages" \ - -H "Content-Type: application/json" \ - -d '{"body":"second-message"}' >/dev/null 2>&1 +ui_jwt POST "/tenants/${UI_TID}/queues/ui-queue-1/messages" '{"body":"second-message"}' >/dev/null 2>&1 || true # Peek — должны видеть оба, без ReceiptHandle -R=$(curl -s --max-time 15 \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages") +R=$(ui_jwt GET "/tenants/${UI_TID}/queues/ui-queue-1/messages") check "UI API: peek — hello-from-ui" "$R" "hello-from-ui" check "UI API: peek — second-message" "$R" "second-message" check_not "UI API: peek — НЕТ receipt_handle" "$R" "receipt_handle" -# Peek не даёт возможности ReceiveMessage через SQS (сообщения не зафиксированы) +# Peek не consumed — SQS receive-message тоже видит сообщения QURL_UI=$(sqs "$UI_AK" "$UI_SK" list-queues 2>&1 \ | python3 -c "import sys,json; d=json.load(sys.stdin); urls=d.get('QueueUrls',[]); print(next((u for u in urls if 'ui-queue-1' in u),''))" 2>/dev/null || true) if [[ -n "$QURL_UI" ]]; then @@ -965,24 +1063,18 @@ echo "" echo "── 26. UI API — purge очереди ──" # ═══════════════════════════════════════════ -R=$(curl -s --max-time 15 -X DELETE \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages") +R=$(ui_jwt DELETE "/tenants/${UI_TID}/queues/ui-queue-1/messages") check "UI API: purge → 200/204 (нет ошибки)" "$R" "^\s*$|purged|ok|{}" # После purge peek должен вернуть пустой массив -R=$(curl -s --max-time 15 \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages") +R=$(ui_jwt GET "/tenants/${UI_TID}/queues/ui-queue-1/messages") check_not "UI API: после purge peek пуст (нет hello-from-ui)" "$R" "hello-from-ui" # Отправим ещё одно после purge — должно работать -R=$(curl -s --max-time 15 -X POST \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages" \ - -H "Content-Type: application/json" \ - -d '{"body":"after-purge"}') +R=$(ui_jwt POST "/tenants/${UI_TID}/queues/ui-queue-1/messages" '{"body":"after-purge"}') check "UI API: send после purge работает" "$R" '"id"' -R=$(curl -s --max-time 15 \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-queue-1/messages") +R=$(ui_jwt GET "/tenants/${UI_TID}/queues/ui-queue-1/messages") check "UI API: peek после purge → after-purge" "$R" "after-purge" echo "" @@ -992,15 +1084,14 @@ echo "── 27. UI API — удаление очереди ──" # ═══════════════════════════════════════════ # Создать ещё одну очередь чтобы удалить -curl -s --max-time 15 -X POST "${BASE_URL}/ui/api/tenants/${UI_TID}/queues" \ - -H "Content-Type: application/json" \ - -d '{"name":"ui-to-delete"}' >/dev/null 2>&1 +ui_jwt POST "/tenants/${UI_TID}/queues" '{"name":"ui-to-delete"}' >/dev/null 2>&1 || true -R=$(curl -s --max-time 15 "${BASE_URL}/ui/api/tenants/${UI_TID}/queues") +R=$(ui_jwt GET "/tenants/${UI_TID}/queues") check "UI API: ui-to-delete создана" "$R" "ui-to-delete" # Удалить HTTP=$(curl -s --max-time 15 -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: Bearer $NUBES_JWT" \ "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-to-delete") if [[ "$HTTP" == "204" || "$HTTP" == "200" ]]; then PASS=$((PASS+1)); TOTAL=$((TOTAL+1)) @@ -1010,11 +1101,12 @@ else echo " ❌ UI API: DELETE /queues/ui-to-delete → HTTP $HTTP" fi -R=$(curl -s --max-time 15 "${BASE_URL}/ui/api/tenants/${UI_TID}/queues") +R=$(ui_jwt GET "/tenants/${UI_TID}/queues") check_not "UI API: ui-to-delete больше не в списке" "$R" "ui-to-delete" # Удаление несуществующей очереди → 404 HTTP=$(curl -s --max-time 15 -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: Bearer $NUBES_JWT" \ "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/no-such-queue") if [[ "$HTTP" == "404" ]]; then PASS=$((PASS+1)); TOTAL=$((TOTAL+1)) @@ -1030,28 +1122,24 @@ echo "" echo "── 28. UI API — изоляция: чужой тенант недоступен ──" # ═══════════════════════════════════════════ -# Создать второго тенанта и попробовать смотреть его очереди через UI API чужим ID +# Создать второго admin тенанта для теста изоляции UI_T2=$(admin_api POST /admin/tenants '{"name":"ui-other-'"$TS"'","max_queues":5}') UI_T2ID=$(echo "$UI_T2" | jq -r '.id') -curl -s --max-time 15 -X POST "${BASE_URL}/ui/api/tenants/${UI_T2ID}/queues" \ - -H "Content-Type: application/json" \ - -d '{"name":"secret-queue"}' >/dev/null 2>&1 -curl -s --max-time 15 -X POST \ - "${BASE_URL}/ui/api/tenants/${UI_T2ID}/queues/secret-queue/messages" \ - -H "Content-Type: application/json" \ - -d '{"body":"secret-data"}' >/dev/null 2>&1 +UI_T2AK=$(echo "$UI_T2" | jq -r '.access_key') +UI_T2SK=$(echo "$UI_T2" | jq -r '.secret_key') -# T1 пытается peek очереди T2 — должен получить 404 (тенант не найден) -HTTP=$(curl -s --max-time 15 -o /dev/null -w "%{http_code}" \ - "${BASE_URL}/ui/api/tenants/${UI_T2ID}/queues/secret-queue/messages") -# UI API публичный (без auth), но очередь принадлежит другому тенанту — данные не должны пересекаться -# Проверяем что данные T2 недоступны через UI T1 -R=$(curl -s --max-time 15 "${BASE_URL}/ui/api/tenants/${UI_TID}/queues") -check_not "UI API: T1 не видит очереди T2" "$R" "secret-queue" +# Создаём очередь во втором тенанте через admin API (не через UI JWT — JWT привязан к одному тенанту) +T2_Q_URL="${BASE_URL}/${UI_T2ID}/secret-queue" +sqs "$UI_T2AK" "$UI_T2SK" create-queue --queue-name "secret-queue" >/dev/null 2>&1 || true +sqs "$UI_T2AK" "$UI_T2SK" send-message --queue-url "$T2_Q_URL" --message-body "secret-data" >/dev/null 2>&1 || true -R_T1_PEEK=$(curl -s --max-time 15 \ - "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/secret-queue/messages") -check "UI API: peek чужой очереди через ID T1 → 404/error" "$R_T1_PEEK" "not found|error|404" +# Текущий JWT тенант не видит очереди другого admin-created тенанта +R=$(ui_jwt GET "/tenants/${UI_TID}/queues") +check_not "UI API: JWT тенант не видит очереди T2" "$R" "secret-queue" + +# peek чужой очереди через JWT тенант → ошибка +R=$(ui_jwt GET "/tenants/${UI_TID}/queues/secret-queue/messages") +check "UI API: peek несуществующей очереди → ошибка" "$R" "not found|error|404" # Cleanup UI тенантов admin_api DELETE "/admin/tenants/${UI_T2ID}" >/dev/null 2>&1 || true diff --git a/tests/quick_test.sh b/tests/quick_test.sh index 4af184f..c52b91d 100755 --- a/tests/quick_test.sh +++ b/tests/quick_test.sh @@ -1,17 +1,20 @@ #!/bin/bash # tests/quick_test.sh — Быстрая проверка shared-sqs (smoke test) # Created: 2026-04-09 -# Покрывает: создание тенанта + очереди, send/receive/delete сообщения, -# UI API peek/send/purge, удаление очереди и тенанта. +# Updated: 2026-04-11 — полная поддержка JWT (nubes api_token), все 17 SQS команд, +# TagQueue/UntagQueue/ListQueueTags/ChangeMessageVisibilityBatch, +# UI API с авторизацией через Bearer JWT. +# Покрывает: health, admin CRUD, все SQS команды, UI API с JWT. # Требования: curl, aws CLI, python3 # Запуск: # bash tests/quick_test.sh -# BASE_URL=https://qu.kube5s.ru ADMIN_TOKEN=... bash tests/quick_test.sh +# BASE_URL=https://qu.kube5s.ru ADMIN_TOKEN=... NUBES_JWT=... bash tests/quick_test.sh set -uo pipefail BASE_URL="${BASE_URL:-https://qu.kube5s.ru}" ADMIN_TOKEN="${ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" +NUBES_JWT="${NUBES_JWT:-eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg}" REGION="us-east-1" TS=$(date +%s) @@ -23,7 +26,7 @@ fail() { echo " ❌ $1"; FAIL=$((FAIL+1)); } check() { local label="$1" body="$2" pattern="$3" - if echo "$body" | grep -qE "$pattern"; then ok "$label"; else fail "$label"; fi + if echo "$body" | grep -qE "$pattern"; then ok "$label"; else fail "$label: $(echo "$body" | head -3)"; fi } check_not() { @@ -36,13 +39,14 @@ check_http() { if [[ "$got" == "$want" ]]; then ok "$label (HTTP $got)"; else fail "$label — ожидалось $want, получено $got"; fi } -# aws CLI с credentials тенанта +# sqs — AWS CLI враппер sqs() { local ak="$1" sk="$2"; shift 2 AWS_ACCESS_KEY_ID="$ak" AWS_SECRET_ACCESS_KEY="$sk" AWS_DEFAULT_REGION="$REGION" \ aws --endpoint-url "$BASE_URL" --output json sqs "$@" 2>&1 } +# admin — Admin API запрос с Bearer admin token admin() { local method="$1" path="$2" body="${3:-}" if [[ -n "$body" ]]; then @@ -57,15 +61,18 @@ admin() { fi } -# ui_api — UI API без авторизации (публичный) +# ui — UI API запрос с Bearer JWT (nubes токен) ui() { local method="$1" path="$2" body="${3:-}" if [[ -n "$body" ]]; then curl -sf --max-time 15 -X "$method" \ + -H "Authorization: Bearer $NUBES_JWT" \ -H "Content-Type: application/json" \ -d "$body" "${BASE_URL}/ui/api${path}" 2>&1 else - curl -sf --max-time 15 -X "$method" "${BASE_URL}/ui/api${path}" 2>&1 + curl -sf --max-time 15 -X "$method" \ + -H "Authorization: Bearer $NUBES_JWT" \ + "${BASE_URL}/ui/api${path}" 2>&1 fi } @@ -78,90 +85,174 @@ echo "" # ── 1. Health ── echo "── 1. Health ──" R=$(curl -sf --max-time 10 "${BASE_URL}/health" 2>&1) -check "GET /health → OK" "$R" "[Oo][Kk]|status" -R=$(ui GET /health) -check "GET /ui/api/health → ok" "$R" "ok" +check "GET /health → ok" "$R" "[Oo][Kk]|status" echo "" -# ── 2. Создание тенанта ── -echo "── 2. Создание тенанта ──" -RESP=$(admin POST /admin/tenants '{"name":"quick-'"$TS"'","max_queues":5}') +# ── 2. UI API авторизация (JWT auth) ── +echo "── 2. UI API авторизация ──" +R=$(curl -sf --max-time 15 -X POST \ + -H "Content-Type: application/json" \ + -d "{\"token\":\"$NUBES_JWT\"}" \ + "${BASE_URL}/ui/api/auth" 2>&1) +check "POST /ui/api/auth → tenant_id" "$R" "tenant_id" +UI_TID=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tenant_id',''))" 2>/dev/null || true) +echo " UI Tenant ID: $UI_TID" +echo "" + +# ── 3. Создание тенанта через Admin API ── +echo "── 3. Admin API — создание тенанта ──" +RESP=$(admin POST /admin/tenants '{"name":"quick-'"$TS"'","max_queues":10}') check "POST /admin/tenants → access_key" "$RESP" "access_key" AK=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_key'])") SK=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['secret_key'])") TID=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") -echo " Tenant ID: $TID" -echo " Access Key: $AK" +echo " Tenant ID: $TID" +echo " Access Key: $AK" echo "" -# ── 3. AWS CLI CRUD ── -echo "── 3. AWS CLI CRUD ──" QNAME="quick-q-$TS" +QURL="${BASE_URL}/${TID}/${QNAME}" +# ── 4. SQS Queue CRUD ── +echo "── 4. SQS Queue CRUD ──" R=$(sqs "$AK" "$SK" create-queue --queue-name "$QNAME") check "CreateQueue → QueueUrl" "$R" "QueueUrl" -QURL=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin)['QueueUrl'])") R=$(sqs "$AK" "$SK" list-queues) -check "ListQueues → очередь $QNAME в списке" "$R" "$QNAME" +check "ListQueues → очередь в списке" "$R" "$QNAME" -R=$(sqs "$AK" "$SK" send-message --queue-url "$QURL" --message-body "hello-quick-$TS") +R=$(sqs "$AK" "$SK" get-queue-url --queue-name "$QNAME") +check "GetQueueUrl → QueueUrl" "$R" "QueueUrl" + +R=$(sqs "$AK" "$SK" get-queue-attributes --queue-url "$QURL" --attribute-names All) +check "GetQueueAttributes → VisibilityTimeout" "$R" "VisibilityTimeout" + +R=$(sqs "$AK" "$SK" set-queue-attributes --queue-url "$QURL" --attributes '{"VisibilityTimeout":"60"}' 2>&1 && echo "OK" || echo "FAIL") +check "SetQueueAttributes → OK" "$R" "OK" +echo "" + +# ── 5. TagQueue / UntagQueue / ListQueueTags ── +echo "── 5. Tags API ──" +R=$(sqs "$AK" "$SK" tag-queue --queue-url "$QURL" --tags env=test,owner=qa 2>&1 && echo "OK" || echo "FAIL") +check "TagQueue → OK" "$R" "OK" + +R=$(sqs "$AK" "$SK" list-queue-tags --queue-url "$QURL") +check "ListQueueTags → env=test" "$R" "env|test" + +R=$(sqs "$AK" "$SK" untag-queue --queue-url "$QURL" --tag-keys env 2>&1 && echo "OK" || echo "FAIL") +check "UntagQueue → OK" "$R" "OK" + +R=$(sqs "$AK" "$SK" list-queue-tags --queue-url "$QURL") +check_not "ListQueueTags → env удалён" "$R" '"env"' +echo "" + +# ── 6. Сообщения: Send / Receive / Delete ── +echo "── 6. Send / Receive / Delete ──" +R=$(sqs "$AK" "$SK" send-message --queue-url "$QURL" --message-body "hello-$TS") check "SendMessage → MessageId" "$R" "MessageId" +check "SendMessage → MD5" "$R" "MD5OfMessageBody" -R=$(sqs "$AK" "$SK" receive-message --queue-url "$QURL") -check "ReceiveMessage → тело сообщения" "$R" "hello-quick-$TS" +R=$(sqs "$AK" "$SK" receive-message --queue-url "$QURL" --visibility-timeout 30) +check "ReceiveMessage → тело" "$R" "hello-$TS" RECEIPT=$(echo "$R" | python3 -c "import sys,json; msgs=json.load(sys.stdin).get('Messages',[]); print(msgs[0]['ReceiptHandle'] if msgs else '')" 2>/dev/null || true) if [[ -n "$RECEIPT" ]]; then + R=$(sqs "$AK" "$SK" change-message-visibility --queue-url "$QURL" --receipt-handle "$RECEIPT" --visibility-timeout 60 2>&1 && echo "OK" || echo "FAIL") + check "ChangeMessageVisibility → OK" "$R" "OK" + sqs "$AK" "$SK" delete-message --queue-url "$QURL" --receipt-handle "$RECEIPT" >/dev/null 2>&1 ok "DeleteMessage → без ошибок" else - fail "DeleteMessage — нет ReceiptHandle" + fail "ReceiptHandle не получен" fi echo "" -# ── 4. UI API — создание очереди ── -echo "── 4. UI API очереди ──" -R=$(ui POST "/tenants/$TID/queues" '{"name":"ui-quick-q"}') -check "UI POST /queues → создана" "$R" "ui-quick-q" +# ── 7. Batch операции ── +echo "── 7. Batch ──" +R=$(sqs "$AK" "$SK" send-message-batch --queue-url "$QURL" \ + --entries '[{"Id":"b1","MessageBody":"batch-1"},{"Id":"b2","MessageBody":"batch-2"},{"Id":"b3","MessageBody":"batch-3"}]') +check "SendMessageBatch → Successful" "$R" "Successful" -R=$(ui GET "/tenants/$TID/queues") -check "UI GET /queues → ui-quick-q в списке" "$R" "ui-quick-q" +R=$(sqs "$AK" "$SK" receive-message --queue-url "$QURL" --max-number-of-messages 3) +RECEIPTS=$(echo "$R" | python3 -c "import sys,json; msgs=json.load(sys.stdin).get('Messages',[]); print('\n'.join(m['ReceiptHandle'] for m in msgs))" 2>/dev/null || true) + +# ChangeMessageVisibilityBatch +if [[ -n "$RECEIPTS" ]]; then + CMV_ENTRIES=$(echo "$RECEIPTS" | python3 -c " +import sys +entries = [] +for i, rh in enumerate(sys.stdin.read().strip().split('\n'), 1): + if rh: + entries.append('{\"Id\":\"cmv%d\",\"ReceiptHandle\":\"%s\",\"VisibilityTimeout\":60}' % (i, rh)) +print('[' + ','.join(entries) + ']') +" 2>/dev/null || echo "[]") + if [[ "$CMV_ENTRIES" != "[]" ]]; then + R=$(sqs "$AK" "$SK" change-message-visibility-batch --queue-url "$QURL" --entries "$CMV_ENTRIES") + check "ChangeMessageVisibilityBatch → Successful" "$R" "Successful" + fi + + DEL_ENTRIES=$(echo "$RECEIPTS" | python3 -c " +import sys +entries = [] +for i, rh in enumerate(sys.stdin.read().strip().split('\n'), 1): + if rh: + entries.append('{\"Id\":\"d%d\",\"ReceiptHandle\":\"%s\"}' % (i, rh)) +print('[' + ','.join(entries) + ']') +" 2>/dev/null || echo "[]") + if [[ "$DEL_ENTRIES" != "[]" ]]; then + R=$(sqs "$AK" "$SK" delete-message-batch --queue-url "$QURL" --entries "$DEL_ENTRIES") + check "DeleteMessageBatch → Successful" "$R" "Successful" + fi +fi echo "" -# ── 5. UI API — send/peek/purge ── -echo "── 5. UI API send/peek/purge ──" -R=$(ui POST "/tenants/$TID/queues/ui-quick-q/messages" '{"body":"msg-a"}') -check "UI POST /messages → id" "$R" '"id"' -ui POST "/tenants/$TID/queues/ui-quick-q/messages" '{"body":"msg-b"}' >/dev/null 2>&1 - -R=$(ui GET "/tenants/$TID/queues/ui-quick-q/messages") -check "UI GET /messages → msg-a" "$R" "msg-a" -check "UI GET /messages → msg-b" "$R" "msg-b" -check_not "UI GET /messages → нет receipt_handle" "$R" "receipt_handle" - -# Purge -ui DELETE "/tenants/$TID/queues/ui-quick-q/messages" >/dev/null 2>&1 -R=$(ui GET "/tenants/$TID/queues/ui-quick-q/messages") -check_not "UI DELETE /messages (purge) → msg-a исчезло" "$R" "msg-a" +# ── 8. PurgeQueue ── +echo "── 8. PurgeQueue ──" +sqs "$AK" "$SK" send-message --queue-url "$QURL" --message-body "purge-me" >/dev/null 2>&1 || true +R=$(sqs "$AK" "$SK" purge-queue --queue-url "$QURL" 2>&1 && echo "OK" || echo "FAIL") +check "PurgeQueue → OK" "$R" "OK" echo "" -# ── 6. UI API — удаление очереди ── -echo "── 6. UI API удаление очереди ──" -HTTP=$(curl -sf --max-time 15 -o /dev/null -w "%{http_code}" -X DELETE \ - "${BASE_URL}/ui/api/tenants/${TID}/queues/ui-quick-q") -check_http "UI DELETE /queues/ui-quick-q → 204/200" "204" "$HTTP" 2>/dev/null || \ -check_http "UI DELETE /queues/ui-quick-q → 200" "200" "$HTTP" +# ── 9. UI API с JWT ── +echo "── 9. UI API с JWT ──" +R=$(ui GET /health) +check "GET /ui/api/health → ok" "$R" "ok|status" -R=$(ui GET "/tenants/$TID/queues") -check_not "Очередь ui-quick-q исчезла из списка" "$R" "ui-quick-q" +if [[ -n "$UI_TID" ]]; then + R=$(ui GET "/tenants/${UI_TID}/queues") + check "UI GET /tenants/{id}/queues → ответ" "$R" "\[|\]|name|queues" + + R=$(ui POST "/tenants/${UI_TID}/queues" '{"name":"ui-q-'"$TS"'"}') + check "UI POST /queues → создана" "$R" "ui-q-$TS|name" + + R=$(ui POST "/tenants/${UI_TID}/queues/ui-q-${TS}/messages" '{"body":"ui-msg-'"$TS"'"}') + check "UI POST /messages → id" "$R" '"id"' + + R=$(ui GET "/tenants/${UI_TID}/queues/ui-q-${TS}/messages") + check "UI GET /messages → тело" "$R" "ui-msg-$TS" + check_not "UI GET /messages → нет receipt_handle" "$R" "receipt_handle" + + R=$(ui DELETE "/tenants/${UI_TID}/queues/ui-q-${TS}/messages" "") + check_not "UI DELETE /messages (purge) → сообщение исчезло" \ + "$(ui GET "/tenants/${UI_TID}/queues/ui-q-${TS}/messages")" "ui-msg-$TS" + + HTTP=$(curl -sf --max-time 15 -o /dev/null -w "%{http_code}" -X DELETE \ + -H "Authorization: Bearer $NUBES_JWT" \ + "${BASE_URL}/ui/api/tenants/${UI_TID}/queues/ui-q-${TS}") + if [[ "$HTTP" == "204" || "$HTTP" == "200" ]]; then + ok "UI DELETE /queues/ui-q → HTTP $HTTP" + else + fail "UI DELETE /queues/ui-q → HTTP $HTTP (ожидалось 200/204)" + fi +else + fail "UI API: JWT auth не дал tenant_id — пропускаем UI тесты" +fi echo "" -# ── 7. AWS CLI DeleteQueue ── -echo "── 7. DeleteQueue ──" -AWS_ACCESS_KEY_ID="$AK" AWS_SECRET_ACCESS_KEY="$SK" AWS_DEFAULT_REGION="$REGION" \ - aws --endpoint-url "$BASE_URL" --output json sqs delete-queue --queue-url "$QURL" >/dev/null 2>&1 -if [[ $? -eq 0 ]]; then ok "DeleteQueue → без ошибки"; else fail "DeleteQueue → ошибка"; fi +# ── 10. DeleteQueue ── +echo "── 10. DeleteQueue ──" +R=$(sqs "$AK" "$SK" delete-queue --queue-url "$QURL" 2>&1 && echo "OK" || echo "FAIL") +check "DeleteQueue → OK" "$R" "OK" echo "" # ── Cleanup ── diff --git a/tests/tag_debug_tmp.sh b/tests/tag_debug_tmp.sh new file mode 100644 index 0000000..829c42c --- /dev/null +++ b/tests/tag_debug_tmp.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -e +T=$(curl -s -X POST https://qu.kube5s.ru/admin/tenants \ + -H "Authorization: Bearer sqs-admin-7a7d8bd0c060a75c198d48680f34077a" \ + -H "Content-Type: application/json" \ + -d '{"name":"tag-debug","max_queues":5}') +echo "Tenant response: $T" +AK=$(echo "$T" | jq -r '.access_key') +SK=$(echo "$T" | jq -r '.secret_key') +TID=$(echo "$T" | jq -r '.id') +echo "AK=$AK TID=$TID" + +Q=$(AWS_ACCESS_KEY_ID="$AK" AWS_SECRET_ACCESS_KEY="$SK" AWS_DEFAULT_REGION=us-east-1 \ + aws --endpoint-url https://qu.kube5s.ru --output json sqs create-queue --queue-name dbg-q 2>&1) +echo "CreateQueue: $Q" +QURL=$(echo "$Q" | jq -r '.QueueUrl' 2>/dev/null || true) +echo "QURL=$QURL" + +echo "--- TagQueue raw response ---" +AWS_ACCESS_KEY_ID="$AK" AWS_SECRET_ACCESS_KEY="$SK" AWS_DEFAULT_REGION=us-east-1 \ + aws --endpoint-url https://qu.kube5s.ru --debug sqs tag-queue --queue-url "$QURL" --tags env=test 2>&1 | grep -E "HTTP|body|Error|400|tag" | head -30 + +# Cleanup +curl -s -X DELETE "https://qu.kube5s.ru/admin/tenants/${TID}" \ + -H "Authorization: Bearer sqs-admin-7a7d8bd0c060a75c198d48680f34077a" > /dev/null +echo "Cleaned up"