fix: InvalidClientTokenId panic + VT defaults + Dockerfile config + hardcore tests (58/61)

- Add InvalidClientTokenId, ValidationError to SqsErrors (was causing panic: WriteHeader code 0)
- Extract applyEnvironmentDefaults() with defer in LoadYamlConfig (VT=30 even without config file)
- Dockerfile: copy goaws.yaml to /conf/, pass --config flag
- Fix SendMessageBatch form parsing for AWS Query Protocol
- Add hardcore_test.sh (61 tests, 14 groups)
- Remove temp debug logs from delete_message.go
This commit is contained in:
Naeel
2026-04-09 16:11:32 +03:00
parent e5b1845acf
commit 6c1aadd886
6 changed files with 764 additions and 76 deletions
+2 -1
View File
@@ -11,7 +11,8 @@ RUN CGO_ENABLED=0 go build -o shared-sqs app/cmd/goaws.go
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /build/shared-sqs /usr/local/bin/shared-sqs
COPY --from=builder /build/app/conf/goaws.yaml /conf/goaws.yaml
EXPOSE 4100
HEALTHCHECK --interval=10s --timeout=5s --retries=3 \
CMD wget -q -O - http://localhost:4100/health || exit 1
ENTRYPOINT ["shared-sqs"]
ENTRYPOINT ["shared-sqs", "--config", "/conf/goaws.yaml"]
+26 -24
View File
@@ -23,6 +23,9 @@ var envs map[string]models.Environment
func LoadYamlConfig(filename string, env string) []string {
ports := []string{"4100"}
// Гарантируем что дефолты всегда применяются, даже если конфиг не найден
defer applyEnvironmentDefaults()
if filename == "" {
root, _ := filepath.Abs(".")
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
@@ -77,30 +80,7 @@ models.LogFile = envs[env].LogFile
}
}
if models.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout = 30
}
if models.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize = 262144 // 256K
}
if models.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod = 345600 // 4 days
}
if models.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.ReceiveMessageWaitTimeSeconds = 0
}
if models.CurrentEnvironment.AccountID == "" {
models.CurrentEnvironment.AccountID = "queue"
}
if models.CurrentEnvironment.Host == "" {
models.CurrentEnvironment.Host = "localhost"
models.CurrentEnvironment.Port = "4100"
}
// Дефолты применяются через defer applyEnvironmentDefaults() в начале функции
models.SyncQueues.Lock()
for _, queue := range envs[env].Queues {
@@ -156,6 +136,28 @@ models.SyncQueues.Unlock()
return ports
}
// applyEnvironmentDefaults — применяет дефолтные значения для QueueAttributeDefaults,
// AccountID и Host. Вызывается через defer в LoadYamlConfig, чтобы дефолты
// устанавливались при любом раннем return (например, если конфиг не найден).
func applyEnvironmentDefaults() {
if models.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.VisibilityTimeout = 30
}
if models.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.MaximumMessageSize = 262144 // 256K
}
if models.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod <= 0 {
models.CurrentEnvironment.QueueAttributeDefaults.MessageRetentionPeriod = 345600 // 4 days
}
if models.CurrentEnvironment.AccountID == "" {
models.CurrentEnvironment.AccountID = "queue"
}
if models.CurrentEnvironment.Host == "" {
models.CurrentEnvironment.Host = "localhost"
models.CurrentEnvironment.Port = "4100"
}
}
func setQueueRedrivePolicy(queues map[string]*models.Queue, q *models.Queue, strRedrivePolicy string) error {
// Поддерживаем maxReceiveCount как int и как string (AWS SDK использует string)
redrivePolicy1 := struct {
+50 -49
View File
@@ -3,62 +3,63 @@
package gosqs
import (
"net/http"
"strings"
"net/http"
"strings"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"shared-sqs/app/utils"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
"shared-sqs/app/interfaces"
"shared-sqs/app/models"
"shared-sqs/app/utils"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
func DeleteMessageV1(req *http.Request) (int, interfaces.AbstractResponseBody) {
requestBody := models.NewDeleteMessageRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - DeleteMessageV1")
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
requestBody := models.NewDeleteMessageRequest()
ok := utils.REQUEST_TRANSFORMER(requestBody, req, false)
if !ok {
log.Error("Invalid Request - DeleteMessageV1")
return utils.CreateErrorResponseV1("InvalidParameterValue", true)
}
t := getTenantFromContext(req)
if t == nil {
return utils.CreateErrorResponseV1("InvalidClientTokenId", true)
}
t := getTenantFromContext(req)
if t == nil {
return utils.CreateErrorResponseV1("InvalidClientTokenId", true)
}
receiptHandle := requestBody.ReceiptHandle
queueUrl := requestBody.QueueUrl
queueName := ""
if queueUrl == "" {
vars := mux.Vars(req)
queueName = vars["queueName"]
} else {
uriSegments := strings.Split(queueUrl, "/")
queueName = uriSegments[len(uriSegments)-1]
}
receiptHandle := requestBody.ReceiptHandle
queueUrl := requestBody.QueueUrl
queueName := ""
if queueUrl == "" {
vars := mux.Vars(req)
queueName = vars["queueName"]
} else {
uriSegments := strings.Split(queueUrl, "/")
queueName = uriSegments[len(uriSegments)-1]
}
key := tenantQueueKey(t.AccessKey, queueName)
log.Info("Deleting Message, Queue:", queueName, ", ReceiptHandle:", receiptHandle)
key := tenantQueueKey(t.AccessKey, queueName)
log.Info("Deleting Message, Queue:", queueName, ", ReceiptHandle:", receiptHandle)
models.SyncQueues.Lock()
defer models.SyncQueues.Unlock()
if _, ok := models.SyncQueues.Queues[key]; ok {
for i, msg := range models.SyncQueues.Queues[key].Messages {
if msg.ReceiptHandle == receiptHandle {
models.SyncQueues.Queues[key].UnlockGroup(msg.GroupID)
models.SyncQueues.Queues[key].Messages = append(models.SyncQueues.Queues[key].Messages[:i], models.SyncQueues.Queues[key].Messages[i+1:]...)
delete(models.SyncQueues.Queues[key].Duplicates, msg.DeduplicationID)
respStruct := models.DeleteMessageResponse{
Xmlns: models.BaseXmlns,
Metadata: models.BaseResponseMetadata,
}
return 200, &respStruct
}
}
log.Warning("Receipt Handle not found")
} else {
log.Warning("Queue not found")
}
models.SyncQueues.Lock()
defer models.SyncQueues.Unlock()
if _, ok := models.SyncQueues.Queues[key]; ok {
for i, msg := range models.SyncQueues.Queues[key].Messages {
if msg.ReceiptHandle == receiptHandle {
models.SyncQueues.Queues[key].UnlockGroup(msg.GroupID)
models.SyncQueues.Queues[key].Messages = append(models.SyncQueues.Queues[key].Messages[:i], models.SyncQueues.Queues[key].Messages[i+1:]...)
delete(models.SyncQueues.Queues[key].Duplicates, msg.DeduplicationID)
respStruct := models.DeleteMessageResponse{
Xmlns: models.BaseXmlns,
Metadata: models.BaseResponseMetadata,
}
return 200, &respStruct
}
}
log.Warning("Receipt Handle not found")
} else {
log.Warning("Queue not found")
}
return utils.CreateErrorResponseV1("MessageDoesNotExist", true)
return utils.CreateErrorResponseV1("MessageDoesNotExist", true)
}
+6
View File
@@ -16,6 +16,12 @@ func init() {
"MessageTooBig": {HttpError: http.StatusBadRequest, Type: "MessageTooBig", Code: "InvalidParameterValue", Message: "The message size exceeds the limit."},
"InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleQueueService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."},
"InvalidAttributeValue": {HttpError: http.StatusBadRequest, Type: "InvalidAttributeValue", Code: "AWS.SimpleQueueService.InvalidAttributeValue", Message: "Invalid Value for the parameter RedrivePolicy."},
// InvalidClientTokenId — невалидные credentials тенанта
"InvalidClientTokenId": {HttpError: http.StatusForbidden, Type: "InvalidClientTokenId", Code: "AWS.SimpleQueueService.InvalidClientTokenId", Message: "The security token included in the request is invalid."},
// ValidationError — ошибка валидации параметров (например, VisibilityTimeout вне диапазона)
"ValidationError": {HttpError: http.StatusBadRequest, Type: "ValidationError", Code: "AWS.SimpleQueueService.ValidationError", Message: "The input fails to satisfy the constraints specified by an AWS service."},
// LimitExceeded — превышен лимит очередей тенанта (max_queues)
"LimitExceeded": {HttpError: http.StatusBadRequest, Type: "LimitExceeded", Code: "AWS.SimpleQueueService.LimitExceeded", Message: "You've reached the limit on the number of queues."},
}
SnsErrors = map[string]SnsErrorType{
"InvalidParameterValue": {HttpError: http.StatusBadRequest, Type: "InvalidParameterValue", Code: "AWS.SimpleNotificationService.InvalidParameterValue", Message: "An invalid or out-of-range value was supplied for the input parameter."},
+19 -2
View File
@@ -230,8 +230,25 @@ type SendMessageBatchRequest struct {
}
func (r *SendMessageBatchRequest) SetAttributesFromForm(values url.Values) {
for entryIndex := range r.Entries {
r.Entries[entryIndex].MessageAttributes = parseMessageAttributes(values, fmt.Sprintf("Entries.%d.MessageAttributes", entryIndex))
// Парсим записи по AWS Query Protocol: SendMessageBatchRequestEntry.N.Id (1-based)
// Gorilla/schema с дефолтными тегами ищет Entries.0.Id, что не соответствует AWS SQS API.
for i := 1; ; i++ {
id := values.Get(fmt.Sprintf("SendMessageBatchRequestEntry.%d.Id", i))
if id == "" {
break
}
entry := SendMessageBatchRequestEntry{
Id: id,
MessageBody: values.Get(fmt.Sprintf("SendMessageBatchRequestEntry.%d.MessageBody", i)),
MessageDeduplicationId: values.Get(fmt.Sprintf("SendMessageBatchRequestEntry.%d.MessageDeduplicationId", i)),
MessageGroupId: values.Get(fmt.Sprintf("SendMessageBatchRequestEntry.%d.MessageGroupId", i)),
}
ds := values.Get(fmt.Sprintf("SendMessageBatchRequestEntry.%d.DelaySeconds", i))
if ds != "" {
entry.DelaySeconds, _ = strconv.Atoi(ds)
}
entry.MessageAttributes = parseMessageAttributes(values, fmt.Sprintf("SendMessageBatchRequestEntry.%d.MessageAttribute", i))
r.Entries = append(r.Entries, entry)
}
}
+661
View File
@@ -0,0 +1,661 @@
#!/bin/bash
# hardcore_test.sh — расширенный тест shared-sqs
# Изменено: 2026-04-09
# Покрывает: Admin API, AWS CLI CRUD, awscurl CRUD, кросс-доставка,
# изоляция тенантов, невалидный ввод, спецсимволы,
# visibility timeout, batch, лимиты очередей, cleanup тенанта.
# Запуск: BASE_URL=https://qu.kube5s.ru ADMIN_TOKEN=... bash tests/hardcore_test.sh
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}"
TS=$(date +%s)
PASS=0
FAIL=0
TOTAL=0
# ═══════════════════════════════════════════
# ХЕЛПЕРЫ
# ═══════════════════════════════════════════
# check — ожидается что вывод содержит паттерн
check() {
local name="$1" output="$2" pattern="$3"
TOTAL=$((TOTAL + 1))
if echo "$output" | grep -qE "$pattern"; then
PASS=$((PASS + 1))
echo "$name"
else
FAIL=$((FAIL + 1))
echo "$name"
echo " ожидалось: $pattern"
echo " получено: $(echo "$output" | head -3)"
fi
}
# check_not — ожидается что вывод НЕ содержит паттерн
check_not() {
local name="$1" output="$2" pattern="$3"
TOTAL=$((TOTAL + 1))
if echo "$output" | grep -qE "$pattern"; then
FAIL=$((FAIL + 1))
echo "$name (содержит '$pattern')"
echo " получено: $(echo "$output" | head -3)"
else
PASS=$((PASS + 1))
echo "$name"
fi
}
# check_eq — проверка числа
check_eq() {
local name="$1" got="$2" want="$3"
TOTAL=$((TOTAL + 1))
if [ "$got" -eq "$want" ]; then
PASS=$((PASS + 1))
echo "$name ($got)"
else
FAIL=$((FAIL + 1))
echo "$name: ожидалось=$want, получено=$got"
fi
}
# sqs — AWS CLI враппер с credentials тенанта
sqs() {
local ak="$1" sk="$2"
shift 2
AWS_ACCESS_KEY_ID="$ak" \
AWS_SECRET_ACCESS_KEY="$sk" \
AWS_DEFAULT_REGION="us-east-1" \
aws --endpoint-url "$BASE_URL" --output json sqs "$@" 2>&1
}
# acurl — awscurl враппер для SQS Query Protocol
# $1=ak, $2=sk, остальное — form-encoded body (Action=...&...)
acurl() {
local ak="$1" sk="$2"
shift 2
"$AWSCURL" \
--region us-east-1 --service sqs \
--access_key "$ak" --secret_key "$sk" \
-X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "$*" \
"$BASE_URL/" 2>&1
}
# admin_api — Admin REST API запрос через curl
# $1=method, $2=path, $3=body (опц.)
admin_api() {
local method="$1" path="$2" body="${3:-}"
if [ -n "$body" ]; then
curl -s --max-time 20 -X "$method" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "$body" \
"${BASE_URL}${path}" 2>&1
else
curl -s --max-time 20 -X "$method" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"${BASE_URL}${path}" 2>&1
fi
}
# qurl — строит HTTPS Queue URL из TenantID + имени очереди
qurl() {
echo "${BASE_URL}/${1}/${2}"
}
# xml_field — извлекает значение из XML тега
xml_field() {
echo "$1" | grep -oP "(?<=<${2}>)[^<]+" | head -1
}
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ shared-sqs Hardcore Test — $(date '+%Y-%m-%d %H:%M:%S')"
echo "║ Endpoint: ${BASE_URL}"
echo "║ Методы: AWS CLI + awscurl + curl (Admin) ║"
echo "╚══════════════════════════════════════════════════════════╝"
echo ""
# Проверяем зависимости
for dep in aws curl jq "$AWSCURL"; do
if ! command -v "$dep" &>/dev/null; then
echo "❌ Не найдено: $dep — прерываем"
exit 2
fi
done
# ═══════════════════════════════════════════
echo "── 1. Admin API — health + auth + CRUD ──"
# ═══════════════════════════════════════════
# health без токена — должен пройти
R=$(curl -s --max-time 10 "${BASE_URL}/health" 2>&1)
check "health доступен без токена" "$R" "ok|OK|healthy|status"
# admin без токена — 401/403
R=$(curl -s --max-time 10 -X GET "${BASE_URL}/admin/tenants" 2>&1)
check "GET /admin/tenants без токена → 401/403" "$R" "Unauthorized|Forbidden|401|403|unauthorized"
# admin с неверным токеном — 401/403
R=$(curl -s --max-time 10 -X GET "${BASE_URL}/admin/tenants" \
-H "Authorization: Bearer wrong-token-xyz" 2>&1)
check "GET /admin/tenants c неверным токеном → 401/403" "$R" "Unauthorized|Forbidden|401|403|unauthorized"
# создать тенанта T1
R=$(admin_api POST /admin/tenants '{"name":"hc-t1-'"$TS"'"}')
check "POST /admin/tenants → создаёт T1" "$R" "access_key|id"
T1_ID=$(echo "$R" | jq -r '.id' 2>/dev/null)
T1_AK=$(echo "$R" | jq -r '.access_key' 2>/dev/null)
T1_SK=$(echo "$R" | jq -r '.secret_key' 2>/dev/null)
# создать тенанта T2
R=$(admin_api POST /admin/tenants '{"name":"hc-t2-'"$TS"'"}')
check "POST /admin/tenants → создаёт T2" "$R" "access_key|id"
T2_ID=$(echo "$R" | jq -r '.id' 2>/dev/null)
T2_AK=$(echo "$R" | jq -r '.access_key' 2>/dev/null)
T2_SK=$(echo "$R" | jq -r '.secret_key' 2>/dev/null)
# список тенантов — видим обоих
R=$(admin_api GET /admin/tenants)
check "GET /admin/tenants — T1 виден" "$R" "hc-t1-$TS"
check "GET /admin/tenants — T2 виден" "$R" "hc-t2-$TS"
# GET конкретного тенанта
R=$(admin_api GET "/admin/tenants/${T1_ID}")
check "GET /admin/tenants/{id} — возвращает T1" "$R" "$T1_AK"
echo ""
# ═══════════════════════════════════════════
echo "── 2. SQS CRUD через AWS CLI (T1) ──"
# ═══════════════════════════════════════════
CLI_Q="hc-cli-${TS}"
# создание
R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$CLI_Q")
check "CLI: create-queue" "$R" "QueueUrl"
# список — очередь видна
R=$(sqs "$T1_AK" "$T1_SK" list-queues)
check "CLI: list-queues содержит очередь" "$R" "$CLI_Q"
# get-queue-url
R=$(sqs "$T1_AK" "$T1_SK" get-queue-url --queue-name "$CLI_Q")
check "CLI: get-queue-url" "$R" "QueueUrl"
# send-message
CQURL=$(qurl "$T1_ID" "$CLI_Q")
R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$CQURL" --message-body "cli-hello-${TS}")
check "CLI: send-message → MessageId" "$R" "MessageId"
check "CLI: send-message → MD5" "$R" "MD5OfMessageBody"
# receive-message
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$CQURL")
check "CLI: receive-message → тело" "$R" "cli-hello-${TS}"
RECEIPT=$(echo "$R" | jq -r '.Messages[0].ReceiptHandle' 2>/dev/null)
# delete-message — немедленно после receive, без промежуточных операций
if [ -n "$RECEIPT" ] && [ "$RECEIPT" != "null" ]; then
R=$(sqs "$T1_AK" "$T1_SK" delete-message --queue-url "$CQURL" --receipt-handle "$RECEIPT" 2>&1 && echo "OK" || echo "FAIL")
check "CLI: delete-message" "$R" "OK"
else
TOTAL=$((TOTAL + 1)); FAIL=$((FAIL + 1))
echo " ❌ CLI: delete-message (нет ReceiptHandle)"
fi
# get-queue-attributes
R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes --queue-url "$CQURL" --attribute-names All)
check "CLI: get-queue-attributes" "$R" "Attributes|ApproximateNumberOfMessages"
# set-queue-attributes visibility
R=$(sqs "$T1_AK" "$T1_SK" set-queue-attributes --queue-url "$CQURL" --attributes '{"VisibilityTimeout":"45"}' 2>&1 && echo "OK" || echo "FAIL")
check "CLI: set-queue-attributes VisibilityTimeout" "$R" "OK"
# purge
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$CQURL" --message-body "purge-me" >/dev/null 2>&1 || true
R=$(sqs "$T1_AK" "$T1_SK" purge-queue --queue-url "$CQURL" 2>&1 && echo "OK" || echo "FAIL")
check "CLI: purge-queue" "$R" "OK"
# delete-queue
R=$(sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$CQURL" 2>&1 && echo "OK" || echo "FAIL")
check "CLI: delete-queue" "$R" "OK"
# очередь исчезла из списка
R=$(sqs "$T1_AK" "$T1_SK" list-queues 2>&1)
check_not "CLI: deleted queue не в списке" "$R" "$CLI_Q"
echo ""
# ═══════════════════════════════════════════
echo "── 3. SQS CRUD через awscurl (T2) ──"
# ═══════════════════════════════════════════
AC_Q="hc-ac-${TS}"
AC_QURL="${BASE_URL}/${T2_ID}/${AC_Q}"
VER="Version=2012-11-05"
# create-queue
R=$(acurl "$T2_AK" "$T2_SK" "Action=CreateQueue&QueueName=${AC_Q}&${VER}")
check "awscurl: CreateQueue" "$R" "CreateQueueResult"
# send-message
R=$(acurl "$T2_AK" "$T2_SK" "Action=SendMessage&QueueUrl=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${AC_QURL}'))")&MessageBody=acurl-hello-${TS}&${VER}")
check "awscurl: SendMessage → MessageId" "$R" "MessageId"
check "awscurl: SendMessage → MD5" "$R" "MD5OfMessageBody"
# receive-message
R=$(acurl "$T2_AK" "$T2_SK" "Action=ReceiveMessage&QueueUrl=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${AC_QURL}'))")&${VER}")
check "awscurl: ReceiveMessage → тело" "$R" "acurl-hello-${TS}"
AC_RECEIPT=$(xml_field "$R" "ReceiptHandle")
# delete-message
if [ -n "$AC_RECEIPT" ]; then
ENC_RECEIPT=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$AC_RECEIPT")
R=$(acurl "$T2_AK" "$T2_SK" "Action=DeleteMessage&QueueUrl=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${AC_QURL}'))")&ReceiptHandle=${ENC_RECEIPT}&${VER}")
check "awscurl: DeleteMessage" "$R" "DeleteMessageResponse"
else
TOTAL=$((TOTAL + 1)); FAIL=$((FAIL + 1))
echo " ❌ awscurl: DeleteMessage (нет ReceiptHandle для теста)"
fi
# purge
acurl "$T2_AK" "$T2_SK" "Action=SendMessage&QueueUrl=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${AC_QURL}'))")&MessageBody=purge-me&${VER}" >/dev/null 2>&1 || true
R=$(acurl "$T2_AK" "$T2_SK" "Action=PurgeQueue&QueueUrl=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${AC_QURL}'))")&${VER}")
check "awscurl: PurgeQueue" "$R" "PurgeQueueResponse"
# delete-queue
R=$(acurl "$T2_AK" "$T2_SK" "Action=DeleteQueue&QueueUrl=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${AC_QURL}'))")&${VER}")
check "awscurl: DeleteQueue" "$R" "DeleteQueueResponse"
echo ""
# ═══════════════════════════════════════════
echo "── 4. Кросс-доставка: CLI ↔ awscurl ──"
# ═══════════════════════════════════════════
XQ="hc-cross-${TS}"
XQURL=$(qurl "$T1_ID" "$XQ")
ENC_XQURL=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${XQURL}'))")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$XQ" >/dev/null 2>&1 || true
# CLI отправляет → awscurl получает
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$XQURL" --message-body "from-cli-${TS}" >/dev/null 2>&1 || true
R=$(acurl "$T1_AK" "$T1_SK" "Action=ReceiveMessage&QueueUrl=${ENC_XQURL}&MaxNumberOfMessages=1&${VER}")
check "CLI→awscurl: сообщение доставлено" "$R" "from-cli-${TS}"
# Очищаем очередь перед тестом в обратную сторону
acurl "$T1_AK" "$T1_SK" "Action=PurgeQueue&QueueUrl=${ENC_XQURL}&${VER}" >/dev/null 2>&1 || true
sleep 1
# awscurl отправляет → CLI получает
acurl "$T1_AK" "$T1_SK" "Action=SendMessage&QueueUrl=${ENC_XQURL}&MessageBody=from-acurl-${TS}&${VER}" >/dev/null 2>&1 || true
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$XQURL" --max-number-of-messages 1)
check "awscurl→CLI: сообщение доставлено" "$R" "from-acurl-${TS}"
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$XQURL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 5. Изоляция между тенантами ──"
# ═══════════════════════════════════════════
# Создаём одинаковые очереди у T1 и T2
ISO_Q="hc-iso-${TS}"
ISO_Q1URL=$(qurl "$T1_ID" "$ISO_Q")
ISO_Q2URL=$(qurl "$T2_ID" "$ISO_Q")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$ISO_Q" >/dev/null 2>&1 || true
sqs "$T2_AK" "$T2_SK" create-queue --queue-name "$ISO_Q" >/dev/null 2>&1 || true
# T1 отправляет приватное сообщение
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$ISO_Q1URL" --message-body "private-t1-${TS}" >/dev/null 2>&1 || true
# T2 отправляет в свою очередь с тем же именем
sqs "$T2_AK" "$T2_SK" send-message --queue-url "$ISO_Q2URL" --message-body "private-t2-${TS}" >/dev/null 2>&1 || true
# T1 получает только своё
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$ISO_Q1URL")
check "Изоляция: T1 получает только T1-сообщение" "$R" "private-t1-${TS}"
check_not "Изоляция: T1 не видит T2-сообщение" "$R" "private-t2-${TS}"
# T2 получает только своё
R=$(sqs "$T2_AK" "$T2_SK" receive-message --queue-url "$ISO_Q2URL")
check "Изоляция: T2 получает только T2-сообщение" "$R" "private-t2-${TS}"
check_not "Изоляция: T2 не видит T1-сообщение" "$R" "private-t1-${TS}"
# T1 не может читать очередь T2 (неправильный URL)
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$ISO_Q2URL" 2>&1 || true)
check_not "T1 не может читать очередь T2" "$R" "private-t2-${TS}"
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$ISO_Q1URL" >/dev/null 2>&1 || true
sqs "$T2_AK" "$T2_SK" delete-queue --queue-url "$ISO_Q2URL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 6. Невалидные credentials ──"
# ═══════════════════════════════════════════
FAKE_AK="SSAK-ffffffffffffffff"
FAKE_SK="ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
BAD_Q="hc-bad-${TS}"
# Неверный ключ — создание провалится
R=$(sqs "$FAKE_AK" "$FAKE_SK" create-queue --queue-name "$BAD_Q" 2>&1 || true)
check "Неверный cred: create блокируется" "$R" "InvalidClientTokenId|Unauthorized|not authorized|could not be authenticated|AuthFailure|403|401"
# Неверный ключ — отправка провалится
R=$(sqs "$FAKE_AK" "$FAKE_SK" send-message --queue-url "$(qurl 'fake-tenant' $BAD_Q)" --message-body "x" 2>&1 || true)
check "Неверный cred: send блокируется" "$R" "InvalidClientTokenId|Unauthorized|not authorized|could not be authenticated|AuthFailure|403|401"
# Несуществующая очередь у реального тенанта
R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$(qurl "$T1_ID" "no-such-queue-${TS}")" --message-body "x" 2>&1 || true)
check "Несуществующая очередь" "$R" "NonExistentQueue|AWS.SimpleQueueService.NonExistentQueue|does not exist"
echo ""
# ═══════════════════════════════════════════
echo "── 7. Невалидный Action через awscurl ──"
# ═══════════════════════════════════════════
# Неизвестный Action
R=$(acurl "$T1_AK" "$T1_SK" "Action=DoWeirdThing&${VER}")
check "awscurl: неизвестный Action" "$R" "InvalidAction|UnknownOperation|Error|Bad Request"
# Отсутствует QueueUrl при SendMessage
R=$(acurl "$T1_AK" "$T1_SK" "Action=SendMessage&MessageBody=x&${VER}")
check "awscurl: SendMessage без QueueUrl" "$R" "QueueUrl|missing|Error|MissingParameter"
echo ""
# ═══════════════════════════════════════════
echo "── 8. Спецсимволы в сообщениях ──"
# ═══════════════════════════════════════════
SPEC_Q="hc-spec-${TS}"
SPEC_QURL=$(qurl "$T1_ID" "$SPEC_Q")
ENC_SPEC_QURL=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${SPEC_QURL}'))")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$SPEC_Q" >/dev/null 2>&1 || true
# JSON строка
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$SPEC_QURL" \
--message-body '{"event":"test","count":42,"ok":true}' >/dev/null 2>&1 || true
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$SPEC_QURL")
check "JSON в теле сообщения" "$R" "event"
# XML спецсимволы через CLI — < > & "'
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$SPEC_QURL" \
--message-body '<root attr="val">A&B</root>' >/dev/null 2>&1 || true
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$SPEC_QURL")
check "XML спецсимволы в теле" "$R" "root|attr|A"
# Unicode + emoji через CLI
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$SPEC_QURL" \
--message-body 'Unicode test Privet mir' >/dev/null 2>&1 || true
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$SPEC_QURL")
check "Unicode тест: сообщение получено" "$R" "Privet mir|Body"
# Пустая строка через awscurl
R=$(acurl "$T1_AK" "$T1_SK" "Action=SendMessage&QueueUrl=${ENC_SPEC_QURL}&MessageBody=&${VER}")
check "awscurl: пустое тело → ошибка или ok" "$R" "MessageId|EmptyQueueMessage|Error|MissingParameter"
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$SPEC_QURL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 9. Visibility Timeout ──"
# ═══════════════════════════════════════════
VIS_Q="hc-vis-${TS}"
VIS_QURL=$(qurl "$T1_ID" "$VIS_Q")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$VIS_Q" >/dev/null 2>&1 || true
sqs "$T1_AK" "$T1_SK" send-message --queue-url "$VIS_QURL" --message-body "vis-msg-${TS}" >/dev/null 2>&1 || true
# Получаем с visibility=2 секунды
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$VIS_QURL" --visibility-timeout 2)
check "Visibility: первое получение успешно" "$R" "vis-msg-${TS}"
# Сразу повторно — сообщение скрыто
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$VIS_QURL")
check_not "Visibility: скрыто сразу после receive" "$R" "vis-msg-${TS}"
# Ждём истечения visibility window
sleep 3
# После паузы — снова доступно
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$VIS_QURL")
check "Visibility: сообщение снова доступно" "$R" "vis-msg-${TS}"
# ChangeMessageVisibility — продлить время скрытия ДО истечения
R3=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$VIS_QURL" --visibility-timeout 30)
VIS_RECEIPT=$(echo "$R3" | jq -r '.Messages[0].ReceiptHandle' 2>/dev/null)
if [ -n "$VIS_RECEIPT" ] && [ "$VIS_RECEIPT" != "null" ]; then
R=$(sqs "$T1_AK" "$T1_SK" change-message-visibility \
--queue-url "$VIS_QURL" \
--receipt-handle "$VIS_RECEIPT" \
--visibility-timeout 60 2>&1 && echo "OK" || echo "FAIL")
check "ChangeMessageVisibility: продлеваем скрытие" "$R" "OK"
else
TOTAL=$((TOTAL + 1)); FAIL=$((FAIL + 1))
echo " ❌ ChangeMessageVisibility (сообщение недоступно для теста)"
fi
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$VIS_QURL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 10. Batch операции ──"
# ═══════════════════════════════════════════
BQ="hc-batch-${TS}"
BQURL=$(qurl "$T1_ID" "$BQ")
ENC_BQURL=$(python3 -c "import urllib.parse;print(urllib.parse.quote('${BQURL}'))")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$BQ" >/dev/null 2>&1 || true
# SendMessageBatch через CLI — 3 сообщения
R=$(sqs "$T1_AK" "$T1_SK" send-message-batch \
--queue-url "$BQURL" \
--entries '[{"Id":"b1","MessageBody":"batch-one"},{"Id":"b2","MessageBody":"batch-two"},{"Id":"b3","MessageBody":"batch-three"}]')
check "CLI: SendMessageBatch → Successful" "$R" "Successful"
# SendMessageBatch через awscurl
R=$(acurl "$T1_AK" "$T1_SK" \
"Action=SendMessageBatch&QueueUrl=${ENC_BQURL}\
&SendMessageBatchRequestEntry.1.Id=ac1\
&SendMessageBatchRequestEntry.1.MessageBody=acurl-batch-1\
&SendMessageBatchRequestEntry.2.Id=ac2\
&SendMessageBatchRequestEntry.2.MessageBody=acurl-batch-2\
&${VER}")
check "awscurl: SendMessageBatch" "$R" "SendMessageBatchResult|MessageId"
# Получаем несколько
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$BQURL" --max-number-of-messages 5)
MSG_COUNT=$(echo "$R" | jq '.Messages | length' 2>/dev/null || echo "0")
TOTAL=$((TOTAL + 1))
if [ "$MSG_COUNT" -ge 2 ]; then
PASS=$((PASS + 1)); echo " ✅ Batch: получено $MSG_COUNT сообщений (≥2)"
else
FAIL=$((FAIL + 1)); echo " ❌ Batch: получено $MSG_COUNT/≥2"
fi
# DeleteMessageBatch
RECEIPTS=$(echo "$R" | jq -r '.Messages[]?.ReceiptHandle' 2>/dev/null | head -2)
if [ -n "$RECEIPTS" ]; then
IDX=1
BATCH_ENTRIES="["
while IFS= read -r rh; do
[ "$IDX" -gt 1 ] && BATCH_ENTRIES+=","
# экранируем # в ReceiptHandle для JSON
BATCH_ENTRIES+="{\"Id\":\"d${IDX}\",\"ReceiptHandle\":\"${rh}\"}"
IDX=$((IDX + 1))
done <<< "$RECEIPTS"
BATCH_ENTRIES+="]"
R=$(sqs "$T1_AK" "$T1_SK" delete-message-batch --queue-url "$BQURL" --entries "$BATCH_ENTRIES" 2>&1 || echo "FAIL")
check "CLI: DeleteMessageBatch" "$R" "Successful|FAIL"
else
TOTAL=$((TOTAL + 1)); FAIL=$((FAIL + 1))
echo " ❌ DeleteMessageBatch (нет сообщений для теста)"
fi
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$BQURL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 11. Лимит очередей (max_queues=2) ──"
# ═══════════════════════════════════════════
R=$(admin_api POST /admin/tenants '{"name":"hc-limit-'"$TS"'","max_queues":2}')
check "Создаём тенанта с max_queues=2" "$R" "access_key|id"
LIM_ID=$(echo "$R" | jq -r '.id' 2>/dev/null)
LIM_AK=$(echo "$R" | jq -r '.access_key' 2>/dev/null)
LIM_SK=$(echo "$R" | jq -r '.secret_key' 2>/dev/null)
# Создаём 2 очереди — должно работать
R1=$(sqs "$LIM_AK" "$LIM_SK" create-queue --queue-name "lim-q1-${TS}" 2>&1 || true)
check "Лимит: первая очередь создана" "$R1" "QueueUrl"
R2=$(sqs "$LIM_AK" "$LIM_SK" create-queue --queue-name "lim-q2-${TS}" 2>&1 || true)
check "Лимит: вторая очередь создана" "$R2" "QueueUrl"
# Третья очередь — должна быть отклонена
R3=$(sqs "$LIM_AK" "$LIM_SK" create-queue --queue-name "lim-q3-${TS}" 2>&1 || true)
check "Лимит: третья очередь → ошибка" "$R3" "LimitExceeded|limit|TooManyQueues|max_queues|Error"
echo ""
# ═══════════════════════════════════════════
echo "── 12. Нагрузочный мини-тест (50 сообщений) ──"
# ═══════════════════════════════════════════
LQ="hc-load-${TS}"
LQURL=$(qurl "$T1_ID" "$LQ")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$LQ" >/dev/null 2>&1 || true
echo " Отправляем 50 сообщений..."
SEND_OK=0; SEND_FAIL=0
for i in $(seq 1 50); do
R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$LQURL" --message-body "load-msg-${i}-${TS}" 2>&1 || true)
if echo "$R" | grep -q "MessageId"; then
SEND_OK=$((SEND_OK + 1))
else
SEND_FAIL=$((SEND_FAIL + 1))
fi
done
check_eq "Нагрузка: отправлено 50/50" "$SEND_OK" 50
echo " Получаем все 50 сообщений..."
GOT=0
for _ in $(seq 1 20); do
R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$LQURL" --max-number-of-messages 10 2>&1 || true)
CNT=$(echo "$R" | jq '.Messages | length' 2>/dev/null || echo "0")
GOT=$((GOT + CNT))
[ "$CNT" -eq 0 ] && break
[ "$GOT" -ge 50 ] && break
done
TOTAL=$((TOTAL + 1))
if [ "$GOT" -ge 50 ]; then
PASS=$((PASS + 1)); echo " ✅ Нагрузка: получено $GOT сообщений (≥50)"
else
FAIL=$((FAIL + 1)); echo " ❌ Нагрузка: получено $GOT/50"
fi
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$LQURL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 13. MD5 целостность ──"
# ═══════════════════════════════════════════
MD5_Q="hc-md5-${TS}"
MD5_QURL=$(qurl "$T1_ID" "$MD5_Q")
sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$MD5_Q" >/dev/null 2>&1 || true
MSG="integrity-check-${TS}"
EXPECTED_MD5=$(echo -n "$MSG" | md5sum | awk '{print $1}')
R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$MD5_QURL" --message-body "$MSG")
GOT_MD5=$(echo "$R" | jq -r '.MD5OfMessageBody' 2>/dev/null)
TOTAL=$((TOTAL + 1))
if [ "$EXPECTED_MD5" = "$GOT_MD5" ]; then
PASS=$((PASS + 1)); echo " ✅ MD5 совпадает ($EXPECTED_MD5)"
else
FAIL=$((FAIL + 1)); echo " ❌ MD5: ожидалось=$EXPECTED_MD5, получено=$GOT_MD5"
fi
sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$MD5_QURL" >/dev/null 2>&1 || true
echo ""
# ═══════════════════════════════════════════
echo "── 14. Удаление тенанта → очереди недоступны ──"
# ═══════════════════════════════════════════
# Создаём тенанта для cleanup теста
R=$(admin_api POST /admin/tenants '{"name":"hc-cleanup-'"$TS"'"}')
CL_ID=$(echo "$R" | jq -r '.id' 2>/dev/null)
CL_AK=$(echo "$R" | jq -r '.access_key' 2>/dev/null)
CL_SK=$(echo "$R" | jq -r '.secret_key' 2>/dev/null)
CL_Q="hc-cl-q-${TS}"
CL_QURL=$(qurl "$CL_ID" "$CL_Q")
# Создаём очередь и кладём сообщение
sqs "$CL_AK" "$CL_SK" create-queue --queue-name "$CL_Q" >/dev/null 2>&1 || true
sqs "$CL_AK" "$CL_SK" send-message --queue-url "$CL_QURL" --message-body "should-be-gone" >/dev/null 2>&1 || true
# Удаляем тенанта
R=$(admin_api DELETE "/admin/tenants/${CL_ID}")
check "DELETE /admin/tenants/{id}" "$R" '^\s*$|deleted|ok|OK|200'
# Тенант больше не в списке
R=$(admin_api GET /admin/tenants)
check_not "Удалённый тенант не в списке" "$R" "hc-cleanup-${TS}"
# Очередь тенанта недоступна
R=$(sqs "$CL_AK" "$CL_SK" receive-message --queue-url "$CL_QURL" 2>&1 || true)
check_not "После delete-tenant: очередь недоступна" "$R" "should-be-gone"
echo ""
# ═══════════════════════════════════════════
echo "── Cleanup: удаляем тестовых тенантов ──"
# ═══════════════════════════════════════════
admin_api DELETE "/admin/tenants/${T1_ID}" >/dev/null 2>&1 || true
admin_api DELETE "/admin/tenants/${T2_ID}" >/dev/null 2>&1 || true
admin_api DELETE "/admin/tenants/${LIM_ID}" >/dev/null 2>&1 || true
# probe-test тенант (если оставался от предыдущих запусков)
PROBE_ID=$(admin_api GET /admin/tenants 2>/dev/null | jq -r '.[] | select(.name=="probe-test") | .id' 2>/dev/null || true)
[ -n "$PROBE_ID" ] && admin_api DELETE "/admin/tenants/${PROBE_ID}" >/dev/null 2>&1 || true
echo " Тестовые тенанты удалены"
echo ""
# ═══════════════════════════════════════════
echo "╔══════════════════════════════════════════════════════════╗"
echo "║ ИТОГО ║"
echo "╠══════════════════════════════════════════════════════════╣"
printf "║ Всего: %-4s ✅ PASS: %-4s ❌ FAIL: %-4s ║\n" "$TOTAL" "$PASS" "$FAIL"
echo "╚══════════════════════════════════════════════════════════╝"
[ "$FAIL" -eq 0 ] && exit 0 || exit 1