From 3d9d05f4f6970c7dfcfb8404e313008ea81d5d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Fri, 7 Nov 2025 15:44:23 +0800 Subject: [PATCH] fix: generate report response --- go.mod | 113 +-- go.sum | 287 ++++---- internal/handler/auth/login_handler.go | 7 +- .../handler/auth/refresh_token_handler.go | 7 +- internal/handler/auth/register_handler.go | 7 +- .../auth/request_password_reset_handler.go | 7 +- .../handler/auth/reset_password_handler.go | 7 +- .../verify_password_reset_code_handler.go | 7 +- internal/logic/auth/register_logic.go | 3 + .../user/request_verification_code_logic.go | 2 +- test/.gitignore | 17 + test/Makefile | 321 +++++++++ test/README.md | 164 +++++ test/doc/AI_GUIDE.md | 476 +++++++++++++ test/doc/HOW_TO_RUN_FLOWS.md | 192 ++++++ test/doc/NETWORK_SETUP.md | 143 ++++ test/doc/QUICK_START.md | 132 ++++ test/doc/TEST_RESULTS_GUIDE.md | 264 +++++++ test/docker/.dockerignore | 9 + test/docker/Dockerfile | 13 + test/docker/docker-compose.yml | 62 ++ test/scenarios/apis/auth.js | 643 ++++++++++++++++++ test/scenarios/apis/health.js | 67 ++ test/scenarios/apis/user.js | 341 ++++++++++ test/scenarios/e2e/authentication-flow.js | 242 +++++++ test/scenarios/e2e/user-profile-flow.js | 369 ++++++++++ test/tests/pre/load-auth-test.js | 69 ++ test/tests/pre/load-user-profile-flow-test.js | 59 ++ test/tests/pre/load-user-test.js | 74 ++ test/tests/pre/stress-auth-test.js | 58 ++ test/tests/pre/stress-user-test.js | 69 ++ test/tests/prod/nightly-auth-test.js | 60 ++ test/tests/prod/nightly-health-test.js | 43 ++ test/tests/prod/nightly-user-test.js | 76 +++ test/tests/smoke/smoke-auth-test.js | 76 +++ test/tests/smoke/smoke-health-test.js | 39 ++ .../smoke/smoke-user-profile-flow-test.js | 75 ++ test/tests/smoke/smoke-user-test.js | 75 ++ 38 files changed, 4463 insertions(+), 212 deletions(-) create mode 100644 test/.gitignore create mode 100644 test/Makefile create mode 100644 test/README.md create mode 100644 test/doc/AI_GUIDE.md create mode 100644 test/doc/HOW_TO_RUN_FLOWS.md create mode 100644 test/doc/NETWORK_SETUP.md create mode 100644 test/doc/QUICK_START.md create mode 100644 test/doc/TEST_RESULTS_GUIDE.md create mode 100644 test/docker/.dockerignore create mode 100644 test/docker/Dockerfile create mode 100644 test/docker/docker-compose.yml create mode 100644 test/scenarios/apis/auth.js create mode 100644 test/scenarios/apis/health.js create mode 100644 test/scenarios/apis/user.js create mode 100644 test/scenarios/e2e/authentication-flow.js create mode 100644 test/scenarios/e2e/user-profile-flow.js create mode 100644 test/tests/pre/load-auth-test.js create mode 100644 test/tests/pre/load-user-profile-flow-test.js create mode 100644 test/tests/pre/load-user-test.js create mode 100644 test/tests/pre/stress-auth-test.js create mode 100644 test/tests/pre/stress-user-test.js create mode 100644 test/tests/prod/nightly-auth-test.js create mode 100644 test/tests/prod/nightly-health-test.js create mode 100644 test/tests/prod/nightly-user-test.js create mode 100644 test/tests/smoke/smoke-auth-test.js create mode 100644 test/tests/smoke/smoke-health-test.js create mode 100644 test/tests/smoke/smoke-user-profile-flow-test.js create mode 100644 test/tests/smoke/smoke-user-test.js diff --git a/go.mod b/go.mod index 367ac84..570e0cf 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.25.1 require ( code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 github.com/alicebob/miniredis/v2 v2.35.0 - github.com/aws/aws-sdk-go-v2 v1.39.2 - github.com/aws/aws-sdk-go-v2/credentials v1.18.16 - github.com/aws/aws-sdk-go-v2/service/ses v1.34.5 - github.com/go-playground/validator/v10 v10.27.0 + github.com/aws/aws-sdk-go-v2 v1.39.6 + github.com/aws/aws-sdk-go-v2/credentials v1.18.21 + github.com/aws/aws-sdk-go-v2/service/ses v1.34.9 + github.com/go-playground/validator/v10 v10.28.0 github.com/golang-jwt/jwt/v4 v4.5.2 github.com/matcornic/hermes/v2 v2.1.0 github.com/minchao/go-mitake v1.0.0 @@ -16,31 +16,33 @@ require ( github.com/segmentio/ksuid v1.0.4 github.com/shopspring/decimal v1.4.0 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.39.0 - github.com/zeromicro/go-zero v1.9.1 - go.mongodb.org/mongo-driver/v2 v2.3.0 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/zeromicro/go-zero v1.9.2 + go.mongodb.org/mongo-driver/v2 v2.4.0 go.uber.org/mock v0.6.0 - golang.org/x/crypto v0.42.0 - google.golang.org/grpc v1.75.1 + golang.org/x/crypto v0.43.0 + google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) require ( dario.cat/mergo v1.0.2 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Masterminds/semver v1.4.2 // indirect - github.com/Masterminds/sprig v2.16.0+incompatible // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/PuerkitoBio/goquery v1.5.0 // indirect - github.com/andybalholm/cascadia v1.0.0 // indirect - github.com/aokoli/goutils v1.0.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect - github.com/aws/smithy-go v1.23.0 // indirect + github.com/PuerkitoBio/goquery v1.10.3 // indirect + github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect + github.com/aws/smithy-go v1.23.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/clipperhouse/stringish v0.1.1 // indirect + github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect @@ -49,94 +51,97 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.3.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/ebitengine/purego v0.8.4 // indirect + github.com/ebitengine/purego v0.9.1 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/css v1.0.1 // indirect github.com/grafana/pyroscope-go v1.2.7 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect - github.com/huandu/xstrings v1.2.0 // indirect - github.com/imdario/mergo v0.3.6 // indirect - github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect + github.com/klauspost/compress v1.18.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.1.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect - github.com/moby/term v0.5.0 // indirect + github.com/moby/term v0.5.2 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/olekukonko/tablewriter v0.0.1 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.1.2 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/openzipkin/zipkin-go v0.4.3 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/client_golang v1.21.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect - github.com/redis/go-redis/v9 v9.15.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/shirou/gopsutil/v4 v4.25.6 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/redis/go-redis/v9 v9.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.10 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect - github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 // indirect - github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/vanng822/css v1.0.1 // indirect + github.com/vanng822/go-premailer v1.25.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.37.0 // indirect - go.opentelemetry.io/otel/sdk v1.37.0 // indirect - go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 8d47ee5..8edf7cd 100644 --- a/go.sum +++ b/go.sum @@ -4,34 +4,39 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v2.16.0+incompatible h1:QZbMUPxRQ50EKAq3LFMnxddMu88/EUUG3qmxwtDmPsY= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.16.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= +github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo= +github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= -github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= -github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= -github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY= -github.com/aws/aws-sdk-go-v2/credentials v1.18.16 h1:4JHirI4zp958zC026Sm+V4pSDwW4pwLefKrc0bF2lwI= -github.com/aws/aws-sdk-go-v2/credentials v1.18.16/go.mod h1:qQMtGx9OSw7ty1yLclzLxXCRbrkjWAM7JnObZjmCB7I= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI= -github.com/aws/aws-sdk-go-v2/service/ses v1.34.5 h1:NwOeuOFrWoh4xWKINrmaAK4Vh75jmmY0RAuNjQ6W5Es= -github.com/aws/aws-sdk-go-v2/service/ses v1.34.5/go.mod h1:m3BsMJZD0eqjGIniBzwrNUqG9ZUPquC4hY9FyE2qNFo= -github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= -github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= +github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= +github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= +github.com/aws/aws-sdk-go-v2/service/ses v1.34.9 h1:hrUBTmbCLLQ+X21wdcoK78sjRW3HGspp/vkAL3TkMx4= +github.com/aws/aws-sdk-go-v2/service/ses v1.34.9/go.mod h1:CeGX4LAFCsrBp24qazKmO/dwxghNCGbAoTbi64dGSEM= +github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= +github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -42,6 +47,10 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= +github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= +github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -61,52 +70,52 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= -github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= +github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= -github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= @@ -115,16 +124,17 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= -github.com/huandu/xstrings v1.2.0 h1:yPeWdRnmynF7p+lLYz0H2tthW9lqhMJrQV/U7yy4wX0= github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -136,22 +146,25 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= +github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/matcornic/hermes/v2 v2.1.0 h1:9TDYFBPFv6mcXanaDmRDEp/RTWj0dTTi+LpFnnnfNWc= github.com/matcornic/hermes/v2 v2.1.0/go.mod h1:2+ziJeoyRfaLiATIL8VZ7f9hpzH4oDHqTmn0bhrsgVI= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/minchao/go-mitake v1.0.0 h1:OgfCUkSRftd6sWibpJyeKU3/gPQhq1t0ttHsnoaeVgQ= github.com/minchao/go-mitake v1.0.0/go.mod h1:RAo0TijPUqhM2ZLMqP9x76wsomL11Ud4sDSwRYwbeGU= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= @@ -166,14 +179,21 @@ github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0= +github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -188,8 +208,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk= @@ -202,19 +222,21 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/redis/go-redis/v9 v9.15.0 h1:2jdes0xJxer4h3NUZrZ4OGSntGlXp4WbXju2nOTRXto= github.com/redis/go-redis/v9 v9.15.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= +github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= -github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= -github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shirou/gopsutil/v4 v4.25.10 h1:at8lk/5T1OgtuCp+AwrDofFRjnvosn0nkN2OLQ6g8tA= +github.com/shirou/gopsutil/v4 v4.25.10/go.mod h1:+kSwyC8DRUD9XXEHCAFjK+0nuArFJM0lva+StQAcskM= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -235,16 +257,18 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= -github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04 h1:L0rPdfzq43+NV8rfIx2kA4iSSLRj2jN5ijYHoeXRwvQ= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/vanng822/css v0.0.0-20190504095207-a21e860bcd04/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= -github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe h1:9YnI5plmy+ad6BM+JCLJb2ZV7/TNiE5l7SNKfumYKgc= +github.com/vanng822/css v1.0.1 h1:10yiXc4e8NI8ldU6mSrWmSWMuyWgPr9DZ63RSlsgDw8= +github.com/vanng822/css v1.0.1/go.mod h1:tcnB1voG49QhCrwq1W0w5hhGasvOg+VQp9i9H1rCM1w= github.com/vanng822/go-premailer v0.0.0-20191214114701-be27abe028fe/go.mod h1:JTFJA/t820uFDoyPpErFQ3rb3amdZoPtxcKervG0OE4= +github.com/vanng822/go-premailer v1.25.0 h1:hGHKfroCXrCDTyGVR8o4HCON5/HWvc7C1uocS+VnaZs= +github.com/vanng822/go-premailer v1.25.0/go.mod h1:8WJKIPZtegxqSOA8+eDFx7QNesKmMYfGEIodLTJqrtM= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -253,23 +277,21 @@ github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6 github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -github.com/zeromicro/go-zero v1.9.1 h1:GZCl4jun/ZgZHnSvX3SSNDHf+tEGmEQ8x2Z23xjHa9g= -github.com/zeromicro/go-zero v1.9.1/go.mod h1:bHOl7Xr7EV/iHZWEqsUNJwFc/9WgAMrPpPagYvOaMtY= -go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU= -go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= -go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +github.com/zeromicro/go-zero v1.9.2 h1:ZXOXBIcazZ1pWAMiHyVnDQ3Sxwy7DYPzjE89Qtj9vqM= +github.com/zeromicro/go-zero v1.9.2/go.mod h1:k8YBMEFZKjTd4q/qO5RCW+zDgUlNyAs5vue3P4/Kmn0= +go.mongodb.org/mongo-driver/v2 v2.4.0 h1:Oq6BmUAAFTzMeh6AonuDlgZMuAuEiUxoAD1koK5MuFo= +go.mongodb.org/mongo-driver/v2 v2.4.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= @@ -282,14 +304,14 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA= go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY= go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM= -go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= -go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= -go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= -go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= -go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= -go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= -go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= -go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -300,35 +322,43 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/crypto v0.0.0-20181029175232-7e6ffbd03851/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190225065934-cc5685c2db12/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -336,42 +366,57 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= -google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= -google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= diff --git a/internal/handler/auth/login_handler.go b/internal/handler/auth/login_handler.go index ce699e1..fd68014 100644 --- a/internal/handler/auth/login_handler.go +++ b/internal/handler/auth/login_handler.go @@ -5,7 +5,6 @@ import ( "backend/internal/svc" "backend/internal/types" errs "backend/pkg/library/errors" - "backend/pkg/library/errors/code" "net/http" "github.com/zeromicro/go-zero/rest/httpx" @@ -46,11 +45,7 @@ func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ - Code: code.SUCCESSCode, - Message: code.SUCCESSMessage, - Data: resp, - }) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) } } } diff --git a/internal/handler/auth/refresh_token_handler.go b/internal/handler/auth/refresh_token_handler.go index a4c2d3b..c5810db 100644 --- a/internal/handler/auth/refresh_token_handler.go +++ b/internal/handler/auth/refresh_token_handler.go @@ -2,7 +2,6 @@ package auth import ( errs "backend/pkg/library/errors" - "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -46,11 +45,7 @@ func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ - Code: code.SUCCESSCode, - Message: code.SUCCESSMessage, - Data: resp, - }) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) } } } diff --git a/internal/handler/auth/register_handler.go b/internal/handler/auth/register_handler.go index cbd4dc8..88d2d1c 100644 --- a/internal/handler/auth/register_handler.go +++ b/internal/handler/auth/register_handler.go @@ -2,7 +2,6 @@ package auth import ( errs "backend/pkg/library/errors" - "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -45,11 +44,7 @@ func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ - Code: code.SUCCESSCode, - Message: code.SUCCESSMessage, - Data: resp, - }) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) } } } diff --git a/internal/handler/auth/request_password_reset_handler.go b/internal/handler/auth/request_password_reset_handler.go index b7c345d..1c85eed 100644 --- a/internal/handler/auth/request_password_reset_handler.go +++ b/internal/handler/auth/request_password_reset_handler.go @@ -2,7 +2,6 @@ package auth import ( errs "backend/pkg/library/errors" - "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -46,11 +45,7 @@ func RequestPasswordResetHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ - Code: code.SUCCESSCode, - Message: code.SUCCESSMessage, - Data: resp, - }) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) } } } diff --git a/internal/handler/auth/reset_password_handler.go b/internal/handler/auth/reset_password_handler.go index 8049c49..e603381 100644 --- a/internal/handler/auth/reset_password_handler.go +++ b/internal/handler/auth/reset_password_handler.go @@ -2,7 +2,6 @@ package auth import ( errs "backend/pkg/library/errors" - "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -46,11 +45,7 @@ func ResetPasswordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ - Code: code.SUCCESSCode, - Message: code.SUCCESSMessage, - Data: resp, - }) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) } } } diff --git a/internal/handler/auth/verify_password_reset_code_handler.go b/internal/handler/auth/verify_password_reset_code_handler.go index 52f5de9..2ec4d92 100644 --- a/internal/handler/auth/verify_password_reset_code_handler.go +++ b/internal/handler/auth/verify_password_reset_code_handler.go @@ -2,7 +2,6 @@ package auth import ( errs "backend/pkg/library/errors" - "backend/pkg/library/errors/code" "net/http" "backend/internal/logic/auth" @@ -46,11 +45,7 @@ func VerifyPasswordResetCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc Error: e.Unwrap(), }) } else { - httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, types.Resp{ - Code: code.SUCCESSCode, - Message: code.SUCCESSMessage, - Data: resp, - }) + httpx.WriteJsonCtx(r.Context(), w, http.StatusOK, resp) } } } diff --git a/internal/logic/auth/register_logic.go b/internal/logic/auth/register_logic.go index 61ff21c..4a6ac84 100644 --- a/internal/logic/auth/register_logic.go +++ b/internal/logic/auth/register_logic.go @@ -8,6 +8,7 @@ import ( member "backend/pkg/member/domain/usecase" "backend/pkg/permission/domain/usecase" "context" + "google.golang.org/protobuf/proto" "github.com/zeromicro/go-zero/core/logx" @@ -85,6 +86,8 @@ func (l *RegisterLogic) Register(req *types.LoginReq) (resp *types.LoginResp, er return nil, err } + // TODO 綁定 User Role + // Step 5: 生成 Token req.LoginID = bd.CreateAccountReq.LoginID tk, err := generateToken(l.svcCtx, l.ctx, req, account.UID, l.svcCtx.Config.RoleConfig.DefaultRoleName) diff --git a/internal/logic/user/request_verification_code_logic.go b/internal/logic/user/request_verification_code_logic.go index e4dc3ed..c7e014a 100644 --- a/internal/logic/user/request_verification_code_logic.go +++ b/internal/logic/user/request_verification_code_logic.go @@ -15,7 +15,7 @@ type RequestVerificationCodeLogic struct { svcCtx *svc.ServiceContext } -// 請求發送驗證碼 (用於驗證信箱/手機) +// NewRequestVerificationCodeLogic 請求發送驗證碼 (用於驗證信箱/手機) func NewRequestVerificationCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RequestVerificationCodeLogic { return &RequestVerificationCodeLogic{ Logger: logx.WithContext(ctx), diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..9233d95 --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,17 @@ +# 測試結果文件 +results/*.json +results/*.csv +results/*.log +!results/.gitkeep + +# 日誌文件 +*.log + +# 環境變數文件 +.env +.env.local + +# 系統文件 +.DS_Store +Thumbs.db + diff --git a/test/Makefile b/test/Makefile new file mode 100644 index 0000000..0bbba35 --- /dev/null +++ b/test/Makefile @@ -0,0 +1,321 @@ +.PHONY: help build run smoke load stress nightly clean test + +# 默認目標 +.DEFAULT_GOAL := help + +# 顏色定義 +GREEN := \033[0;32m +YELLOW := \033[0;33m +NC := \033[0m # No Color + +# 環境變數 +BASE_URL ?= http://localhost:8888 +TEST_LOGIN_ID ?= +TEST_PASSWORD ?= +K6_IMAGE ?= k6-test:latest + +help: ## 顯示幫助信息 + @echo "$(GREEN)可用命令:$(NC)" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(YELLOW)%-20s$(NC) %s\n", $$1, $$2}' + + +smoke-auth: ## 運行認證功能冒煙測試 + @echo "$(GREEN)運行認證功能冒煙測試...$(NC)" + @docker run --rm -i \ + --network host \ + -v $(PWD)/scenarios:/app/scenarios \ + -v $(PWD)/tests:/app/tests \ + -e BASE_URL=$(BASE_URL) \ + grafana/k6:latest run /app/tests/smoke/smoke-auth-test.js + +load-auth: ## 運行認證功能負載測試 + @echo "$(GREEN)運行認證功能負載測試...$(NC)" + @docker run --rm -i \ + --network host \ + -v $(PWD)/scenarios:/app/scenarios \ + -v $(PWD)/tests:/app/tests \ + -e BASE_URL=$(BASE_URL) \ + grafana/k6:latest run /app/tests/pre/load-auth-test.js + + +# build: ## 構建 Docker 映像 +# @echo "$(GREEN)構建 k6 測試映像...$(NC)" +# docker-compose build + +# # ==================== 冒煙測試 ==================== +# smoke: smoke-all ## 運行所有冒煙測試 + +# smoke-all: ## 運行所有冒煙測試 +# @echo "$(GREEN)運行所有冒煙測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-health-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-auth-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-user-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-user-profile-flow-test.js + +# smoke-health: ## 運行健康檢查冒煙測試 +# @echo "$(GREEN)運行健康檢查冒煙測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-health-test.js + +# smoke-auth: ## 運行認證功能冒煙測試 +# @echo "$(GREEN)運行認證功能冒煙測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-auth-test.js + +# smoke-user: ## 運行使用者功能冒煙測試 +# @echo "$(GREEN)運行使用者功能冒煙測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/smoke/smoke-user-test.js + +# smoke-user-profile-flow: ## 運行使用者資料流程冒煙測試 +# @echo "$(GREEN)運行使用者資料流程冒煙測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/scenarios/e2e/user-profile-flow.js + +# # ==================== 負載測試 ==================== +# load: load-all ## 運行所有負載測試 + +# load-all: ## 運行所有負載測試 +# @echo "$(GREEN)運行所有負載測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/load-auth-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/load-user-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/load-user-profile-flow-test.js + + + +# load-user: ## 運行使用者功能負載測試 +# @echo "$(GREEN)運行使用者功能負載測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/load-user-test.js + +# load-user-profile-flow: ## 運行使用者資料流程負載測試 +# @echo "$(GREEN)運行使用者資料流程負載測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/load-user-profile-flow-test.js + +# # ==================== 壓力測試 ==================== +# stress: stress-all ## 運行所有壓力測試 + +# stress-all: ## 運行所有壓力測試 +# @echo "$(GREEN)運行所有壓力測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/stress-auth-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/stress-user-test.js + +# stress-auth: ## 運行認證功能壓力測試 +# @echo "$(GREEN)運行認證功能壓力測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/stress-auth-test.js + +# stress-user: ## 運行使用者功能壓力測試 +# @echo "$(GREEN)運行使用者功能壓力測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/pre/stress-user-test.js + +# # ==================== 生產環境測試 ==================== +# nightly: nightly-all ## 運行所有夜間測試 + +# nightly-all: ## 運行所有夜間測試 +# @echo "$(GREEN)運行所有夜間測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run /app/tests/prod/nightly-health-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run /app/tests/prod/nightly-auth-test.js +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run /app/tests/prod/nightly-user-test.js + +# nightly-health: ## 運行健康檢查夜間測試 +# @echo "$(GREEN)運行健康檢查夜間測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# grafana/k6:latest run /app/tests/prod/nightly-health-test.js + +# nightly-auth: ## 運行認證功能夜間測試 +# @echo "$(GREEN)運行認證功能夜間測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run /app/tests/prod/nightly-auth-test.js + +# nightly-user: ## 運行使用者功能夜間測試 +# @echo "$(GREEN)運行使用者功能夜間測試...$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run /app/tests/prod/nightly-user-test.js + +# # ==================== 通用命令 ==================== +# run: ## 運行指定的測試文件 (使用: make run TEST=tests/smoke/smoke-auth-test.js) +# @if [ -z "$(TEST)" ]; then \ +# echo "$(YELLOW)錯誤: 請指定測試文件$(NC)"; \ +# echo "用法: make run TEST=tests/smoke/smoke-auth-test.js"; \ +# exit 1; \ +# fi +# @echo "$(GREEN)運行測試: $(TEST)$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run /app/$(TEST) + +# run-local: ## 本地運行測試(不使用 Docker,需要本地安裝 k6) +# @if [ -z "$(TEST)" ]; then \ +# echo "$(YELLOW)錯誤: 請指定測試文件$(NC)"; \ +# echo "用法: make run-local TEST=tests/smoke/smoke-auth-test.js"; \ +# exit 1; \ +# fi +# @echo "$(GREEN)本地運行測試: $(TEST)$(NC)" +# @BASE_URL=$(BASE_URL) \ +# TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# TEST_PASSWORD=$(TEST_PASSWORD) \ +# k6 run $(TEST) + +# clean: ## 清理 Docker 資源 +# @echo "$(GREEN)清理 Docker 資源...$(NC)" +# @docker-compose down -v 2>/dev/null || true +# @docker rmi $(K6_IMAGE) 2>/dev/null || true +# @echo "$(GREEN)清理完成$(NC)" + +# # ==================== 測試結果輸出 ==================== +# run-with-output: ## 運行測試並輸出結果到文件 (使用: make run-with-output TEST=tests/smoke/smoke-auth-test.js OUTPUT=results.json) +# @if [ -z "$(TEST)" ]; then \ +# echo "$(YELLOW)錯誤: 請指定測試文件$(NC)"; \ +# echo "用法: make run-with-output TEST=tests/smoke/smoke-auth-test.js OUTPUT=results.json"; \ +# exit 1; \ +# fi +# @mkdir -p results +# @echo "$(GREEN)運行測試並輸出結果: $(TEST) -> results/$(OUTPUT)$(NC)" +# @docker run --rm -i \ +# --network host \ +# -v $(PWD)/scenarios:/app/scenarios \ +# -v $(PWD)/tests:/app/tests \ +# -v $(PWD)/results:/app/results \ +# -e BASE_URL=$(BASE_URL) \ +# -e TEST_LOGIN_ID=$(TEST_LOGIN_ID) \ +# -e TEST_PASSWORD=$(TEST_PASSWORD) \ +# grafana/k6:latest run --out json=/app/results/$(OUTPUT) /app/$(TEST) + +# # ==================== 環境設置 ==================== +# env-dev: ## 設置開發環境變數 +# @echo "$(GREEN)設置開發環境...$(NC)" +# @export BASE_URL=https://dev-api.example.com +# @echo "BASE_URL=$(BASE_URL)" + +# env-pre: ## 設置預發布環境變數 +# @echo "$(GREEN)設置預發布環境...$(NC)" +# @export BASE_URL=https://pre-api.example.com +# @echo "BASE_URL=$(BASE_URL)" + +# env-prod: ## 設置生產環境變數 +# @echo "$(GREEN)設置生產環境...$(NC)" +# @export BASE_URL=https://api.example.com +# @echo "BASE_URL=$(BASE_URL)" +# @echo "$(YELLOW)注意: 生產環境測試需要設置 TEST_LOGIN_ID 和 TEST_PASSWORD$(NC)" + diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..6c6424a --- /dev/null +++ b/test/README.md @@ -0,0 +1,164 @@ +# 測試架構說明 + +本目錄包含基於 k6 的負載測試架構,採用模組化設計,支援場景重複使用。 + +## 📁 目錄結構 + +``` +test/ +├── scenarios/ # 可重複使用的場景模組 +│ ├── apis/ # API 層級場景(單一 API 端點) +│ └── e2e/ # 端到端業務流程場景 +├── tests/ # 不同環境的測試配置 +│ ├── smoke/ # 冒煙測試(Dev/QA 環境) +│ ├── pre/ # 預發布環境測試(負載/壓力測試) +│ └── prod/ # 生產環境測試(夜間監控) +├── Dockerfile # Docker 映像定義 +├── docker-compose.yml # Docker Compose 配置 +├── Makefile # Make 命令文件 +├── AI_GUIDE.md # AI 助手指南(如何添加新 API) +└── README.md # 本文件 +``` + +## 🎯 設計原則 + +1. **模組化場景**:每個 API 端點對應一個場景函數,可獨立使用 +2. **可擴展預設行為**:場景函數接受 `options` 參數,可覆蓋預設行為 +3. **自定義指標可選**:支援可選的自定義指標,不強制使用 +4. **請求標籤與檢查**:每個請求添加 tag,並檢查響應結果 +5. **使用 Scenarios 設置工作負載**:提供更大的靈活性 +6. **避免多用途測試**:每個測試文件專注於一個主要目的 + +## 🚀 快速開始 + +### 使用 Makefile(推薦) + +最簡單的方式是使用 Makefile 提供的命令: + +```bash +# 查看所有可用命令 +make help + +# 運行所有冒煙測試 +make smoke + +# 運行特定冒煙測試 +make smoke-health +make smoke-auth +make smoke-user + +# 運行負載測試 +make load +make load-auth +make load-user + +# 運行壓力測試 +make stress +make stress-auth +make stress-user + +# 運行生產環境測試(需要設置環境變數) +export BASE_URL=https://api.example.com +export TEST_LOGIN_ID=test@example.com +export TEST_PASSWORD=TestPassword123! +make nightly + +# 運行指定的測試文件 +make run TEST=tests/smoke/smoke-auth-test.js + +# 運行測試並輸出結果 +make run-with-output TEST=tests/smoke/smoke-auth-test.js OUTPUT=results.json +``` + +### 使用 Docker Compose + +```bash +# 構建 Docker 映像 +docker-compose build + +# 運行測試(需要設置環境變數) +BASE_URL=https://api.example.com docker-compose run --rm k6 run tests/smoke/smoke-auth-test.js +``` + +### 直接使用 k6(需要本地安裝 k6) + +```bash +# 設置環境變數 +export BASE_URL=https://api.example.com +export TEST_LOGIN_ID=test@example.com +export TEST_PASSWORD=TestPassword123! + +# 運行冒煙測試 +k6 run tests/smoke/smoke-auth-test.js +k6 run tests/smoke/smoke-user-test.js +k6 run tests/smoke/smoke-health-test.js + +# 運行負載測試 +k6 run tests/pre/load-auth-test.js +k6 run tests/pre/stress-auth-test.js + +# 運行生產環境測試 +k6 run tests/prod/nightly-auth-test.js +``` + +### 環境變數設置 + +```bash +# 設置 API 基礎 URL(必需) +export BASE_URL=https://api.example.com + +# 生產環境測試帳號(僅用於生產環境測試) +export TEST_LOGIN_ID=test@example.com +export TEST_PASSWORD=TestPassword123! + +# 或在 Makefile 命令中直接指定 +make smoke BASE_URL=https://api.example.com +make nightly BASE_URL=https://api.example.com TEST_LOGIN_ID=test@example.com TEST_PASSWORD=TestPassword123! +``` + +### 網絡配置說明 + +**重要**:如果 API 服務器在主機上運行(不在容器內),測試容器已經配置為使用主機網絡模式(`--network host`),可以直接訪問主機上的服務。 + +```bash +# 如果 API 在本地主機運行 +export BASE_URL=http://localhost:8888 +# 或 +export BASE_URL=https://localhost:8888 + +# 如果 API 在其他地址運行 +export BASE_URL=https://api.example.com + +# 運行測試 +make smoke-health +``` + +**注意**: +- 使用 `--network host` 模式時,容器直接使用主機網絡,可以訪問 `localhost` 和主機上的所有端口 +- 如果 API 使用 HTTPS 但沒有有效證書,可能需要設置 `K6_SKIP_TLS_VERIFY=true` + +## 📝 添加新 API 場景 + +請參考 `AI_GUIDE.md` 文件,其中包含詳細的步驟和模板。 + +快速步驟: +1. 在 `scenarios/apis/` 創建場景模組 +2. 在 `scenarios/e2e/` 創建流程場景(如果需要) +3. 在 `tests/smoke/` 創建冒煙測試 +4. 在 `tests/pre/` 創建負載/壓力測試 +5. 在 `tests/prod/` 創建夜間測試(如果適用) + +## 📊 測試類型 + +| 環境 | 測試類型 | 目的 | 並發數 | 持續時間 | +|------|---------|------|--------|---------| +| Dev/QA | Smoke | 快速驗證功能可用性 | 1 | 30s | +| Pre-release | Load | 正常負載下的性能 | 10-20 | 5-10m | +| Pre-release | Stress | 高負載下的穩定性 | 50-100 | 5-10m | +| Production | Nightly | 監控生產環境穩定性 | 5 | 5-10m | + +## 🔗 相關資源 + +- [k6 官方文檔](https://k6.io/docs/) +- [AI_GUIDE.md](./AI_GUIDE.md) - 詳細的 AI 助手指南 + diff --git a/test/doc/AI_GUIDE.md b/test/doc/AI_GUIDE.md new file mode 100644 index 0000000..38be822 --- /dev/null +++ b/test/doc/AI_GUIDE.md @@ -0,0 +1,476 @@ +# 測試架構 AI 指南 + +本文檔旨在幫助 AI 助手理解此測試架構的設計原則和使用方式,以便在添加新 API 時能夠快速套用相同的模式。 + +## 📁 目錄結構 + +``` +test/ +├── scenarios/ # 可重複使用的場景模組 +│ ├── apis/ # API 層級場景(單一 API 端點) +│ │ ├── auth.js # 認證相關場景 +│ │ ├── user.js # 使用者相關場景 +│ │ └── health.js # 健康檢查場景 +│ └── e2e/ # 端到端業務流程場景 +│ ├── authentication-flow.js # 認證流程 +│ └── user-profile-flow.js # 使用者資料流程 +├── tests/ # 不同環境的測試配置 +│ ├── smoke/ # 冒煙測試(Dev/QA 環境) +│ ├── pre/ # 預發布環境測試(負載/壓力測試) +│ └── prod/ # 生產環境測試(夜間監控) +└── AI_GUIDE.md # 本文件 +``` + +## 🎯 設計原則 + +### 1. 模組化場景設計 +- **場景模組化**:每個 API 端點對應一個場景函數,可獨立使用 +- **可擴展預設行為**:場景函數接受 `options` 參數,可覆蓋預設行為 +- **避免緊密耦合**:場景邏輯與測試配置分離 + +### 2. 自定義指標(可選) +- 場景函數支援可選的 `customMetrics` 參數 +- 如果不提供,使用預設指標 +- 如果需要特殊指標,可以傳入自定義指標對象 + +### 3. 請求標籤與檢查 +- 每個請求都添加 `tags`,便於在 k6 中過濾和分析 +- 使用 `check()` 函數檢查請求結果 +- 檢查項目包括:狀態碼、響應結構、業務邏輯驗證 + +### 4. 使用 Scenarios 設置工作負載 +- 測試文件使用 k6 的 `scenarios` 配置工作負載 +- 不同環境使用不同的 executor 和配置 +- 避免只使用 Groups,使用 Scenarios 提供更大靈活性 + +### 5. 避免多用途測試 +- 每個測試文件專注於一個主要目的 +- 每個環境一個測試文件 +- 這樣可以避免混合責任,並有助於追踪歷史結果 + +## 📝 如何添加新 API 場景 + +### 步驟 1: 在 `scenarios/apis/` 創建或更新場景模組 + +假設要添加一個新的 API 模組 `order.js`(訂單相關): + +```javascript +/** + * 訂單相關 API 場景模組 + */ +import http from 'k6/http'; +import { check } from 'k6'; +import { Rate, Trend } from 'k6/metrics'; + +// 可選的自定義指標 +const createOrderSuccessRate = new Rate('order_create_success'); +const createOrderDuration = new Trend('order_create_duration'); + +/** + * 創建訂單 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {Object} options.orderData - 訂單資料 + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 創建結果 + */ +export function createOrder(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + orderData = {}, + customMetrics = null, + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + const url = `${baseUrl}/api/v1/orders`; + const payload = JSON.stringify(orderData); + + const params = { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + tags: { + name: 'order_create', + api: 'order', + method: 'create_order', + }, + }; + + const startTime = Date.now(); + const res = http.post(url, payload, params); + const duration = Date.now() - startTime; + + const success = check(res, { + 'create order status is 200': (r) => r.status === 200, + 'create order has order_id': (r) => { + try { + const body = JSON.parse(r.body); + return body.order_id && body.order_id.length > 0; + } catch { + return false; + } + }, + }, { name: 'create_order_checks' }); + + if (customMetrics) { + customMetrics.createOrderSuccessRate?.add(success); + customMetrics.createOrderDuration?.add(duration); + } else { + createOrderSuccessRate.add(success); + createOrderDuration.add(duration); + } + + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body); + } catch (e) { + console.error('Failed to parse create order response:', e); + } + } + + return { + success, + status: res.status, + response: result, + }; +} +``` + +### 步驟 2: 在 `scenarios/e2e/` 創建流程場景(如果需要) + +如果有多個 API 需要組合使用,創建流程場景: + +```javascript +/** + * 訂單流程端到端場景 + */ +import * as order from '../apis/order.js'; +import { sleep } from 'k6'; + +/** + * 完整訂單流程(創建 → 查詢 → 取消) + */ +export function orderLifecycleFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + orderData = {}, + } = options; + + // 步驟 1: 創建訂單 + const createResult = order.createOrder({ + baseUrl, + accessToken, + orderData, + }); + + if (!createResult.success || !createResult.response) { + return { + success: false, + step: 'create', + error: 'Create order failed', + createResult, + }; + } + + sleep(1); + + // 步驟 2: 查詢訂單 + const orderId = createResult.response.order_id; + const getResult = order.getOrder({ + baseUrl, + accessToken, + orderId, + }); + + // ... 其他步驟 + + return { + success: true, + createResult, + getResult, + }; +} +``` + +### 步驟 3: 在 `tests/` 創建測試文件 + +#### 3.1 冒煙測試 (`tests/smoke/smoke-order-test.js`) + +```javascript +import { createOrder } from '../../scenarios/apis/order.js'; +import { loginWithCredentials } from '../../scenarios/apis/auth.js'; + +export const options = { + scenarios: { + smoke_order: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '30s', + tags: { test_type: 'smoke', api: 'order' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + http_req_duration: ['p(95)<2000'], + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + + // 1. 登入獲取 Token + const loginResult = loginWithCredentials({ + baseUrl, + loginId: 'test@example.com', + password: 'Test123!', + }); + + if (!loginResult.success || !loginResult.tokens) { + return; + } + + // 2. 創建訂單 + createOrder({ + baseUrl, + accessToken: loginResult.tokens.accessToken, + orderData: { /* ... */ }, + }); +} +``` + +#### 3.2 負載測試 (`tests/pre/load-order-test.js`) + +```javascript +import { createOrder } from '../../scenarios/apis/order.js'; +import { loginWithCredentials } from '../../scenarios/apis/auth.js'; + +export const options = { + scenarios: { + load_order: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '30s', target: 10 }, + { duration: '1m', target: 10 }, + { duration: '30s', target: 20 }, + { duration: '1m', target: 20 }, + { duration: '30s', target: 0 }, + ], + gracefulRampDown: '30s', + tags: { test_type: 'load', api: 'order', environment: 'pre' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], + http_req_duration: ['p(95)<2000'], + http_req_failed: ['rate<0.05'], + }, +}; + +export default function () { + // ... 測試邏輯 +} +``` + +#### 3.3 生產環境測試 (`tests/prod/nightly-order-test.js`) + +```javascript +import { createOrder } from '../../scenarios/apis/order.js'; +import { loginWithCredentials } from '../../scenarios/apis/auth.js'; + +export const options = { + scenarios: { + nightly_order: { + executor: 'constant-vus', + vus: 5, + duration: '5m', + tags: { test_type: 'nightly', api: 'order', environment: 'prod' }, + }, + }, + thresholds: { + checks: ['rate>0.98'], + http_req_duration: ['p(95)<2000'], + http_req_failed: ['rate<0.02'], + }, +}; + +export default function () { + // 注意:生產環境使用預先創建的測試帳號 + const loginId = __ENV.TEST_LOGIN_ID || 'test@example.com'; + const password = __ENV.TEST_PASSWORD || 'TestPassword123!'; + + // ... 測試邏輯 +} +``` + +## 🔑 關鍵模式 + +### 場景函數模板 + +```javascript +export function apiFunctionName(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + // 必需參數 + requiredParam, + // 可選參數 + optionalParam = defaultValue, + // 自定義指標(可選) + customMetrics = null, + } = options; + + // 參數驗證 + if (!requiredParam) { + throw new Error('requiredParam is required'); + } + + // 構建請求 + const url = `${baseUrl}/api/v1/endpoint`; + const payload = JSON.stringify({ /* ... */ }); + + const params = { + headers: { + 'Content-Type': 'application/json', + // 如果需要認證 + 'Authorization': `Bearer ${accessToken}`, + }, + tags: { + name: 'api_function_name', + api: 'api_name', + method: 'method_name', + }, + }; + + // 發送請求 + const startTime = Date.now(); + const res = http.post(url, payload, params); // 或 get, put, delete + const duration = Date.now() - startTime; + + // 檢查結果 + const success = check(res, { + 'status is 200': (r) => r.status === 200, + 'has required field': (r) => { + try { + const body = JSON.parse(r.body); + return body.required_field !== undefined; + } catch { + return false; + } + }, + }, { name: 'api_function_checks' }); + + // 使用指標 + if (customMetrics) { + customMetrics.successRate?.add(success); + customMetrics.duration?.add(duration); + } else { + // 使用預設指標 + } + + // 解析響應 + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body); + } catch (e) { + console.error('Failed to parse response:', e); + } + } + + // 返回結果 + return { + success, + status: res.status, + response: result, + // 其他有用的數據 + }; +} +``` + +### 測試文件模板 + +```javascript +import { apiFunction } from '../../scenarios/apis/api-module.js'; + +export const options = { + scenarios: { + test_name: { + executor: 'executor_type', // shared-iterations, ramping-vus, constant-vus, etc. + // executor 特定配置 + tags: { test_type: 'smoke|load|stress|nightly', api: 'api_name', environment: 'dev|pre|prod' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], // 根據環境調整 + http_req_duration: ['p(95)<2000'], + http_req_failed: ['rate<0.05'], + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + + // 測試邏輯 + const result = apiFunction({ + baseUrl, + // 參數 + }); +} +``` + +## 📊 環境配置 + +### 不同環境的測試配置 + +| 環境 | 測試類型 | Executor | VUs | 持續時間 | 成功率要求 | +|------|---------|----------|-----|---------|-----------| +| Dev/QA | Smoke | shared-iterations | 1 | 30s | 100% | +| Pre-release | Load | ramping-vus | 0-20 | 5-10m | 95% | +| Pre-release | Stress | ramping-vus | 0-100 | 5-10m | 90% | +| Production | Nightly | constant-vus | 5 | 5-10m | 98% | + +## ✅ 檢查清單 + +添加新 API 場景時,請確保: + +- [ ] 在 `scenarios/apis/` 創建場景模組 +- [ ] 場景函數接受 `options` 參數,包含 `baseUrl` 和 `customMetrics` +- [ ] 為請求添加 `tags`,包含 `name`, `api`, `method` +- [ ] 使用 `check()` 驗證響應結果 +- [ ] 支援可選的自定義指標 +- [ ] 返回結構化的結果對象 +- [ ] 如果需要,在 `scenarios/e2e/` 創建流程場景 +- [ ] 在 `tests/smoke/` 創建冒煙測試 +- [ ] 在 `tests/pre/` 創建負載/壓力測試 +- [ ] 在 `tests/prod/` 創建夜間測試(如果適用) +- [ ] 所有測試文件使用適當的 `scenarios` 配置 +- [ ] 設置適當的 `thresholds` + +## 🚀 快速開始 + +1. **查看現有場景**:參考 `scenarios/apis/auth.js` 或 `scenarios/apis/user.js` +2. **複製模板**:使用上面的場景函數模板 +3. **調整參數**:根據新 API 的需求調整 +4. **創建測試**:在對應的 `tests/` 目錄下創建測試文件 +5. **運行測試**:使用 k6 運行測試文件 + +## 📚 參考資源 + +- [k6 官方文檔](https://k6.io/docs/) +- [k6 Scenarios](https://k6.io/docs/using-k6/scenarios/) +- [k6 Thresholds](https://k6.io/docs/using-k6/thresholds/) +- [k6 Tags](https://k6.io/docs/using-k6/tags-and-groups/) + +--- + +**記住**:此架構的核心是**模組化**和**可重複使用**。每個場景應該獨立、可測試,並且可以輕鬆組合形成更複雜的流程。 + diff --git a/test/doc/HOW_TO_RUN_FLOWS.md b/test/doc/HOW_TO_RUN_FLOWS.md new file mode 100644 index 0000000..db1060b --- /dev/null +++ b/test/doc/HOW_TO_RUN_FLOWS.md @@ -0,0 +1,192 @@ +# 如何運行流程測試 + +## 📝 說明 + +`user-profile-flow.js` 是一個**場景模組**,不是可以直接運行的測試文件。它提供了可重複使用的流程函數,需要在測試文件中導入並使用。 + +## 🚀 快速開始 + +### 方式 1: 使用已創建的測試文件(推薦) + +我們已經創建了使用這些流程的測試文件: + +#### 冒煙測試 + +```bash +# 設置 API 地址 +export BASE_URL=http://localhost:8888 + +# 運行使用者資料流程冒煙測試 +make smoke-user-profile-flow +``` + +#### 負載測試 + +```bash +# 設置 API 地址 +export BASE_URL=http://localhost:8888 + +# 運行使用者資料流程負載測試 +make load-user-profile-flow +``` + +### 方式 2: 使用通用 run 命令 + +```bash +# 設置 API 地址 +export BASE_URL=http://localhost:8888 + +# 運行指定的流程測試 +make run TEST=tests/smoke/smoke-user-profile-flow-test.js +``` + +### 方式 3: 直接使用 k6(如果本地已安裝) + +```bash +# 設置 API 地址 +export BASE_URL=http://localhost:8888 + +# 運行測試 +k6 run tests/smoke/smoke-user-profile-flow-test.js +``` + +## 📋 可用的流程函數 + +`user-profile-flow.js` 提供了以下流程函數: + +### 1. `getAndUpdateProfileFlow` +取得並更新個人資料流程 + +```javascript +import { getAndUpdateProfileFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +const result = getAndUpdateProfileFlow({ + baseUrl: 'http://localhost:8888', + accessToken: 'your_access_token', + updateData: { + nickname: 'NewNickname', + preferred_language: 'zh-tw', + }, +}); +``` + +### 2. `emailVerificationFlow` +完整 Email 驗證流程(請求 → 提交) + +```javascript +import { emailVerificationFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +const result = emailVerificationFlow({ + baseUrl: 'http://localhost:8888', + accessToken: 'your_access_token', + verifyCode: '123456', // 需要從外部獲取 +}); +``` + +### 3. `phoneVerificationFlow` +完整手機驗證流程(請求 → 提交) + +```javascript +import { phoneVerificationFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +const result = phoneVerificationFlow({ + baseUrl: 'http://localhost:8888', + accessToken: 'your_access_token', + verifyCode: '123456', // 需要從外部獲取 +}); +``` + +### 4. `passwordChangeFlow` +登入狀態下修改密碼流程 + +```javascript +import { passwordChangeFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +const result = passwordChangeFlow({ + baseUrl: 'http://localhost:8888', + accessToken: 'your_access_token', + currentPassword: 'OldPassword123!', + newPassword: 'NewPassword123!', +}); +``` + +### 5. `userProfileInitializationFlow` +完整的使用者資料初始化流程(註冊 → 登入 → 取得資訊 → 更新資訊) + +```javascript +import { userProfileInitializationFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +const result = userProfileInitializationFlow({ + baseUrl: 'http://localhost:8888', + loginId: 'test@example.com', + password: 'Test123456!', + updateData: { + nickname: 'TestUser', + preferred_language: 'zh-tw', + currency: 'TWD', + }, +}); +``` + +## 📝 創建自定義測試文件 + +如果你想創建自己的測試文件來使用這些流程,可以參考以下模板: + +```javascript +/** + * 自定義流程測試 + */ +import { userProfileInitializationFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +export const options = { + scenarios: { + custom_flow: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '30s', + tags: { test_type: 'custom', flow: 'user_profile' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + http_req_duration: ['p(95)<3000'], + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'http://localhost:8888'; + + // 使用流程函數 + const result = userProfileInitializationFlow({ + baseUrl, + loginId: `test_${Date.now()}@example.com`, + password: 'Test123456!', + }); + + if (!result.success) { + console.error(`Test failed: ${result.step} - ${result.error}`); + return; + } + + console.log('Test passed!'); +} +``` + +然後運行: + +```bash +make run TEST=tests/your-custom-test.js +``` + +## 🎯 測試文件位置 + +- **冒煙測試**: `tests/smoke/smoke-user-profile-flow-test.js` +- **負載測試**: `tests/pre/load-user-profile-flow-test.js` + +## 📚 相關文檔 + +- [AI_GUIDE.md](./AI_GUIDE.md) - 如何添加新 API 場景 +- [README.md](./README.md) - 完整文檔 +- [QUICK_START.md](./QUICK_START.md) - 快速開始指南 + diff --git a/test/doc/NETWORK_SETUP.md b/test/doc/NETWORK_SETUP.md new file mode 100644 index 0000000..3664b77 --- /dev/null +++ b/test/doc/NETWORK_SETUP.md @@ -0,0 +1,143 @@ +# 網絡配置說明 + +## 🐳 Docker 容器訪問主機服務 + +當 API 服務器在主機上運行,而 k6 測試在 Docker 容器內運行時,需要配置網絡讓容器能夠訪問主機上的服務。 + +## ✅ 已配置的解決方案 + +本測試架構已經配置為使用**主機網絡模式**(`--network host`),容器可以直接訪問主機上的服務。 + +### 配置說明 + +1. **Makefile**:所有 `docker run` 命令都添加了 `--network host` 參數 +2. **docker-compose.yml**:k6 服務使用 `network_mode: host` + +### 使用方式 + +```bash +# 如果 API 在本地主機運行(localhost) +export BASE_URL=http://localhost:8888 +# 或 +export BASE_URL=https://localhost:8888 + +# 運行測試 +make smoke-health +``` + +## 🔧 其他網絡配置選項 + +### 選項 1: 使用 host.docker.internal(僅限 macOS/Windows) + +在 macOS 和 Windows 上,Docker Desktop 提供了 `host.docker.internal` 主機名: + +```bash +export BASE_URL=http://host.docker.internal:8888 +make smoke-health +``` + +**注意**:在 Linux 上,`host.docker.internal` 默認不可用,需要額外配置。 + +### 選項 2: 使用主機 IP 地址 + +如果知道主機的 IP 地址,可以直接使用: + +```bash +# 獲取主機 IP 地址 +ip addr show | grep "inet " | grep -v 127.0.0.1 + +# 使用主機 IP +export BASE_URL=http://192.168.1.100:8888 +make smoke-health +``` + +### 選項 3: 使用 Docker 網絡(如果 API 也在容器中) + +如果 API 服務器也在 Docker 容器中運行,可以使用 Docker 網絡: + +```yaml +# docker-compose.yml +services: + api: + # ... API 配置 + + k6: + # ... k6 配置 + networks: + - test-network + extra_hosts: + - "host.docker.internal:host-gateway" +``` + +## 📝 常見問題 + +### Q: 為什麼使用 `--network host`? + +A: `--network host` 是最簡單可靠的方式,讓容器直接使用主機網絡,可以訪問 `localhost` 和主機上的所有端口。 + +### Q: 使用 `--network host` 有什麼限制? + +A: +- 容器無法使用端口映射(因為直接使用主機端口) +- 在 macOS/Windows 上,`--network host` 可能不工作(需要使用 `host.docker.internal`) + +### Q: 如何測試連接? + +A: 在容器內測試連接: + +```bash +# 進入容器 +docker run -it --rm --network host grafana/k6:latest sh + +# 在容器內測試連接 +curl http://localhost:8888/api/v1/health +``` + +### Q: HTTPS 證書問題? + +A: 如果使用 HTTPS 但沒有有效證書,可以設置: + +```bash +export K6_SKIP_TLS_VERIFY=true +make smoke-health +``` + +或在測試文件中設置: + +```javascript +export const options = { + // ... + insecureSkipTLSVerify: true, +}; +``` + +## 🚀 快速測試 + +### 測試本地 API + +```bash +# 1. 確認 API 在本地運行 +curl http://localhost:8888/api/v1/health + +# 2. 設置 BASE_URL +export BASE_URL=http://localhost:8888 + +# 3. 運行測試 +make smoke-health +``` + +### 測試遠程 API + +```bash +# 1. 設置遠程 API 地址 +export BASE_URL=https://api.example.com + +# 2. 運行測試 +make smoke-health +``` + +## 📚 參考資源 + +- [Docker 網絡文檔](https://docs.docker.com/network/) +- [k6 網絡配置](https://k6.io/docs/using-k6/options/#insecureskiptlsverify) + diff --git a/test/doc/QUICK_START.md b/test/doc/QUICK_START.md new file mode 100644 index 0000000..b480f60 --- /dev/null +++ b/test/doc/QUICK_START.md @@ -0,0 +1,132 @@ +# 快速開始指南 + +## 🚀 最簡單的使用方式 + +### 1. 查看所有可用命令 + +```bash +cd test +make help +``` + +### 2. 運行冒煙測試(最常用) + +```bash +# 設置 API 地址 +export BASE_URL=https://api.example.com + +# 運行所有冒煙測試 +make smoke + +# 或運行單個測試 +make smoke-health +make smoke-auth +make smoke-user +``` + +### 3. 運行負載測試 + +```bash +# 設置 API 地址 +export BASE_URL=https://pre-api.example.com + +# 運行負載測試 +make load +make load-auth +make load-user +``` + +### 4. 運行壓力測試 + +```bash +# 設置 API 地址 +export BASE_URL=https://pre-api.example.com + +# 運行壓力測試 +make stress +make stress-auth +make stress-user +``` + +### 5. 運行生產環境測試 + +```bash +# 設置環境變數 +export BASE_URL=https://api.example.com +export TEST_LOGIN_ID=test@example.com +export TEST_PASSWORD=TestPassword123! + +# 運行夜間測試 +make nightly +make nightly-health +make nightly-auth +make nightly-user +``` + +## 📝 常用命令速查 + +| 命令 | 說明 | +|------|------| +| `make help` | 顯示所有可用命令 | +| `make smoke` | 運行所有冒煙測試 | +| `make load` | 運行所有負載測試 | +| `make stress` | 運行所有壓力測試 | +| `make nightly` | 運行所有夜間測試 | +| `make run TEST=tests/smoke/smoke-auth-test.js` | 運行指定測試 | +| `make clean` | 清理 Docker 資源 | + +## 🔧 環境變數設置 + +### 方式 1: 使用 export(推薦) + +```bash +export BASE_URL=https://api.example.com +export TEST_LOGIN_ID=test@example.com +export TEST_PASSWORD=TestPassword123! +make smoke +``` + +### 方式 2: 直接在命令中指定 + +```bash +make smoke BASE_URL=https://api.example.com +make nightly BASE_URL=https://api.example.com TEST_LOGIN_ID=test@example.com TEST_PASSWORD=TestPassword123! +``` + +## 💡 提示 + +1. **首次使用**:不需要構建 Docker 映像,Makefile 會自動使用官方的 `grafana/k6:latest` 映像 +2. **本地運行**:如果本地已安裝 k6,可以使用 `make run-local TEST=...` +3. **查看結果**:測試結果會直接輸出到終端,也可以使用 `make run-with-output` 保存到文件 + +## 🐛 常見問題 + +### Q: 如何運行自定義測試文件? + +A: 使用 `make run` 命令: +```bash +make run TEST=tests/smoke/smoke-auth-test.js +``` + +### Q: 如何設置不同的 API 地址? + +A: 使用環境變數: +```bash +export BASE_URL=https://dev-api.example.com +make smoke +``` + +### Q: 如何查看測試結果? + +A: 測試結果會直接輸出到終端。如果需要保存,使用: +```bash +make run-with-output TEST=tests/smoke/smoke-auth-test.js OUTPUT=results.json +``` + +### Q: 如何清理 Docker 資源? + +A: 使用 `make clean` 命令: +```bash +make clean +``` + diff --git a/test/doc/TEST_RESULTS_GUIDE.md b/test/doc/TEST_RESULTS_GUIDE.md new file mode 100644 index 0000000..ed0ff93 --- /dev/null +++ b/test/doc/TEST_RESULTS_GUIDE.md @@ -0,0 +1,264 @@ +# 測試結果解讀指南 + +## 📊 如何解讀測試結果 + +### ✅ 成功的測試結果特徵 + +1. **所有檢查通過**:`checks_succeeded: 100%` +2. **請求成功**:`http_req_failed: 0%` +3. **閾值通過**:所有閾值顯示 `✓` +4. **響應時間正常**:在預期的時間範圍內 + +### ❌ 失敗的測試結果特徵 + +1. **連接錯誤**:`connection refused`、`timeout`、`no route to host` +2. **HTTP 錯誤**:`http_req_failed > 0%` +3. **檢查失敗**:`checks_succeeded < 100%` +4. **閾值失敗**:閾值顯示 `✗` + +## 🔍 常見錯誤及解決方案 + +### 錯誤 1: Connection Refused + +**錯誤信息:** +``` +dial tcp 127.0.0.1:8888: connect: connection refused +``` + +**原因:** +- API 服務器沒有運行 +- BASE_URL 設置錯誤 +- 服務器運行在不同的端口或地址 + +**解決方案:** + +```bash +# 1. 檢查 API 服務器是否運行 +curl https://api.example.com/api/v1/health + +# 2. 設置正確的 BASE_URL +export BASE_URL=https://api.example.com +make smoke-health + +# 或直接在命令中指定 +make smoke-health BASE_URL=https://api.example.com +``` + +### 錯誤 2: Timeout + +**錯誤信息:** +``` +context deadline exceeded +``` + +**原因:** +- 服務器響應太慢 +- 網絡問題 +- 服務器過載 + +**解決方案:** + +```bash +# 1. 檢查服務器響應時間 +curl -w "@-" -o /dev/null -s https://api.example.com/api/v1/health <<'EOF' + time_namelookup: %{time_namelookup}\n + time_connect: %{time_connect}\n + time_appconnect: %{time_appconnect}\n + time_pretransfer: %{time_pretransfer}\n + time_redirect: %{time_redirect}\n + time_starttransfer: %{time_starttransfer}\n + ----------\n + time_total: %{time_total}\n +EOF + +# 2. 調整測試的閾值(如果需要) +# 編輯測試文件,增加響應時間閾值 +``` + +### 錯誤 3: HTTP 4xx/5xx 錯誤 + +**錯誤信息:** +``` +http_req_failed: 20.00% (4xx/5xx responses) +``` + +**原因:** +- API 端點不存在 +- 認證失敗 +- 服務器內部錯誤 + +**解決方案:** + +```bash +# 1. 檢查 API 端點是否正確 +curl -v https://api.example.com/api/v1/health + +# 2. 檢查認證(如果需要) +curl -H "Authorization: Bearer YOUR_TOKEN" https://api.example.com/api/v1/health + +# 3. 查看服務器日誌 +# 檢查應用程序的日誌文件 +``` + +### 錯誤 4: 閾值失敗 + +**錯誤信息:** +``` +✗ 'rate==1.0' rate=95.00% +``` + +**原因:** +- 部分請求失敗 +- 部分檢查未通過 +- 性能未達標 + +**解決方案:** + +```bash +# 1. 查看詳細的失敗原因 +# 檢查輸出中的具體錯誤信息 + +# 2. 調整閾值(如果合理) +# 編輯測試文件,調整閾值要求 +# 例如:從 'rate==1.0' 改為 'rate>0.95' + +# 3. 修復根本問題 +# 如果是服務器問題,需要修復服務器 +``` + +## 📈 測試結果指標說明 + +### HTTP 指標 + +- **http_reqs**: 總請求數 +- **http_req_duration**: 請求持續時間 + - `avg`: 平均時間 + - `min`: 最短時間 + - `max`: 最長時間 + - `p(90)`: 90% 的請求在此時間內完成 + - `p(95)`: 95% 的請求在此時間內完成 +- **http_req_failed**: 失敗的請求百分比 + +### 檢查指標 + +- **checks_total**: 總檢查數 +- **checks_succeeded**: 成功的檢查數 +- **checks_failed**: 失敗的檢查數 + +### 執行指標 + +- **iterations**: 迭代次數 +- **iteration_duration**: 每次迭代的持續時間 +- **vus**: 虛擬用戶數 + +### 網絡指標 + +- **data_received**: 接收的數據量 +- **data_sent**: 發送的數據量 + +## 🎯 閾值說明 + +閾值(Thresholds)是測試的通過標準: + +- **`rate==1.0`**: 100% 必須通過 +- **`rate>0.95`**: 至少 95% 必須通過 +- **`p(95)<2000`**: 95% 的請求必須在 2000ms 內完成 +- **`rate<0.05`**: 失敗率必須低於 5% + +## 💡 最佳實踐 + +1. **先運行健康檢查**:確保服務器可用 + ```bash + make smoke-health BASE_URL=https://api.example.com + ``` + +2. **逐步增加負載**:從冒煙測試開始,然後負載測試,最後壓力測試 + +3. **監控關鍵指標**: + - 響應時間 + - 錯誤率 + - 吞吐量 + +4. **設置合理的閾值**: + - 開發環境:較寬鬆的閾值 + - 生產環境:嚴格的閾值 + +5. **保存測試結果**: + ```bash + make run-with-output TEST=tests/smoke/smoke-health-test.js OUTPUT=results.json + ``` + +## 🔧 調試技巧 + +### 1. 使用詳細輸出 + +```bash +# 查看詳細的請求信息 +k6 run --http-debug tests/smoke/smoke-health-test.js +``` + +### 2. 檢查網絡連接 + +```bash +# 測試連接 +curl -v https://api.example.com/api/v1/health + +# 檢查 DNS +nslookup api.example.com + +# 檢查端口 +telnet api.example.com 443 +``` + +### 3. 查看 Docker 日誌 + +```bash +# 如果使用 Docker,查看容器日誌 +docker logs k6-test +``` + +### 4. 本地測試 + +```bash +# 如果本地安裝了 k6,可以直接運行 +export BASE_URL=https://api.example.com +k6 run tests/smoke/smoke-health-test.js +``` + +## 📝 示例:解讀你的測試結果 + +### 你的測試結果分析 + +``` +✗ checks: rate==1.0 (實際: 50.00%) + - 10 個檢查中,5 個通過,5 個失敗 + - 狀態碼檢查:0% 通過(因為連接失敗) + - 響應時間檢查:100% 通過(沒有實際請求) + +✗ health_check_success: rate==1.0 (實際: 0.00%) + - 所有健康檢查都失敗了 + +✓ http_req_duration: p(95)<500 (實際: 0s) + - 沒有實際請求,所以響應時間為 0 + +http_req_failed: 100.00% (5 out of 5) + - 所有請求都失敗了 +``` + +### 解決方案 + +```bash +# 1. 確認 API 服務器地址 +# 例如:https://dev-api.example.com 或 https://localhost:8888 + +# 2. 設置 BASE_URL +export BASE_URL=https://dev-api.example.com + +# 3. 重新運行測試 +make smoke-health + +# 或如果服務器在本地運行 +export BASE_URL=http://localhost:8888 +make smoke-health +``` + diff --git a/test/docker/.dockerignore b/test/docker/.dockerignore new file mode 100644 index 0000000..4b25875 --- /dev/null +++ b/test/docker/.dockerignore @@ -0,0 +1,9 @@ +# 忽略不需要的文件 +results/ +*.log +.DS_Store +.git/ +.gitignore +README.md +AI_GUIDE.md + diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile new file mode 100644 index 0000000..90a84ea --- /dev/null +++ b/test/docker/Dockerfile @@ -0,0 +1,13 @@ +# k6 測試環境 Dockerfile +FROM grafana/k6:latest + +# 設置工作目錄 +WORKDIR /app + +# 複製測試文件 +COPY scenarios/ /app/scenarios/ +COPY tests/ /app/tests/ + +# 設置默認命令 +CMD ["run", "tests/smoke/smoke-health-test.js"] + diff --git a/test/docker/docker-compose.yml b/test/docker/docker-compose.yml new file mode 100644 index 0000000..210120a --- /dev/null +++ b/test/docker/docker-compose.yml @@ -0,0 +1,62 @@ +version: '3.8' + +services: + k6: + build: + context: . + dockerfile: Dockerfile + image: k6-test:latest + container_name: k6-test + network_mode: host # 使用主機網絡,讓容器可以訪問主機上的服務 + volumes: + - ./scenarios:/app/scenarios + - ./tests:/app/tests + - ./results:/app/results + environment: + - BASE_URL=${BASE_URL:-https://localhost:8888} + - TEST_LOGIN_ID=${TEST_LOGIN_ID:-} + - TEST_PASSWORD=${TEST_PASSWORD:-} + # 不自動啟動,通過 make 命令運行 + command: ["run", "tests/smoke/smoke-health-test.js"] + + # 可選:如果需要測試資料庫或其他服務 + # influxdb: + # image: influxdb:2.7 + # container_name: k6-influxdb + # ports: + # - "8086:8086" + # environment: + # - DOCKER_INFLUXDB_INIT_MODE=setup + # - DOCKER_INFLUXDB_INIT_USERNAME=admin + # - DOCKER_INFLUXDB_INIT_PASSWORD=admin123456 + # - DOCKER_INFLUXDB_INIT_ORG=myorg + # - DOCKER_INFLUXDB_INIT_BUCKET=mybucket + # volumes: + # - influxdb-data:/var/lib/influxdb2 + # networks: + # - test-network + + # grafana: + # image: grafana/grafana:latest + # container_name: k6-grafana + # ports: + # - "3000:3000" + # environment: + # - GF_AUTH_ANONYMOUS_ENABLED=true + # - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + # volumes: + # - grafana-data:/var/lib/grafana + # - ./provisioning:/etc/grafana/provisioning + # networks: + # - test-network + # depends_on: + # - influxdb + +networks: + test-network: + driver: bridge + +# volumes: +# influxdb-data: +# grafana-data: + diff --git a/test/scenarios/apis/auth.js b/test/scenarios/apis/auth.js new file mode 100644 index 0000000..9ce4cb2 --- /dev/null +++ b/test/scenarios/apis/auth.js @@ -0,0 +1,643 @@ +/** + * 認證相關 API 場景模組 + * + * 此模組提供可重複使用的認證相關場景,包括: + * - 註冊(帳號密碼、第三方平台) + * - 登入(帳號密碼、第三方平台) + * - Token 刷新 + * - 密碼重設流程 + * + * 使用方式: + * import { registerWithCredentials, loginWithCredentials } from './scenarios/apis/auth.js'; + */ + +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { Rate, Trend } from 'k6/metrics'; + +// 可選的自定義指標 +const registerSuccessRate = new Rate('auth_register_success'); +const loginSuccessRate = new Rate('auth_login_success'); +const registerDuration = new Trend('auth_register_duration'); +const loginDuration = new Trend('auth_login_duration'); + +/** + * 使用帳號密碼註冊 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID(email 或 phone) + * @param {string} options.password - 密碼 + * @param {string} options.accountType - 帳號類型(email/phone/any) + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 註冊結果,包含 tokens 和響應 + */ +export function registerWithCredentials(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'http://localhost:8888', + loginId = `test_${Date.now()}@example.com`, + password = 'Test123456!', + accountType = 'email', + customMetrics = null, + } = options; + + const url = `${baseUrl}/api/v1/auth/register`; + const payload = JSON.stringify({ + auth_method: 'credentials', + login_id: loginId, + credentials: { + password: password, + password_confirm: password, + account_type: accountType, + }, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_register_credentials', + api: 'auth', + method: 'register', + auth_type: 'credentials', + }, + }; + + const startTime = Date.now(); + const res = http.post(url, payload, params); + const duration = Date.now() - startTime; + + // 解析響應 + let result = null; + let responseData = null; + + if (res.status === 200) { + try { + result = JSON.parse(res.body); + + // 支持兩種響應格式: + // 1. 直接返回 LoginResp: { access_token, refresh_token, uid, ... } + // 2. 包裝在 Resp 中: { code, message, data: { access_token, ... } } + if (result.data && typeof result.data === 'object') { + // 格式 2: 包裝在 Resp 中 + responseData = result.data; + } else if (result.access_token) { + // 格式 1: 直接返回 LoginResp + responseData = result; + } else { + // 無法識別的格式,記錄響應以便調試 + console.warn('Unexpected response format. Full response:', JSON.stringify(result)); + console.warn('Response keys:', Object.keys(result)); + responseData = result; + } + } catch (e) { + console.error('Failed to parse register response:', e); + console.error('Response body:', res.body); + } + } + + // 檢查響應結果 + const success = check(res, { + 'register status is 200': (r) => r.status === 200, + 'register has access_token': () => { + return responseData && responseData.access_token && responseData.access_token.length > 0; + }, + 'register has refresh_token': () => { + return responseData && responseData.refresh_token && responseData.refresh_token.length > 0; + }, + 'register has uid': () => { + return responseData && responseData.uid && responseData.uid.length > 0; + }, + }, { name: 'register_checks' }); + + // 使用自定義指標(如果提供) + if (customMetrics) { + customMetrics.registerSuccessRate.add(success); + customMetrics.registerDuration.add(duration); + } else { + // 使用預設指標 + registerSuccessRate.add(success); + registerDuration.add(duration); + } + + return { + success, + status: res.status, + response: result, + responseData: responseData, + tokens: responseData && responseData.access_token ? { + accessToken: responseData.access_token, + refreshToken: responseData.refresh_token, + uid: responseData.uid, + } : null, + }; +} + +/** + * 使用第三方平台註冊 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID + * @param {string} options.provider - 平台名稱(google/line/apple) + * @param {string} options.token - 平台提供的 Token + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 註冊結果 + */ +export function registerWithPlatform(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + loginId = `platform_${Date.now()}@example.com`, + provider = 'google', + token = 'mock_platform_token', + customMetrics = null, + } = options; + + const url = `${baseUrl}/api/v1/auth/register`; + const payload = JSON.stringify({ + auth_method: 'platform', + login_id: loginId, + platform: { + provider: provider, + token: token, + }, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_register_platform', + api: 'auth', + method: 'register', + auth_type: 'platform', + provider: provider, + }, + }; + + const startTime = Date.now(); + const res = http.post(url, payload, params); + const duration = Date.now() - startTime; + + const success = check(res, { + 'register platform status is 200': (r) => r.status === 200, + 'register platform has tokens': (r) => { + try { + const body = JSON.parse(r.body); + return body.access_token && body.refresh_token; + } catch { + return false; + } + }, + }, { name: 'register_platform_checks' }); + + if (customMetrics) { + customMetrics.registerSuccessRate?.add(success); + customMetrics.registerDuration?.add(duration); + } else { + registerSuccessRate.add(success); + registerDuration.add(duration); + } + + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body); + } catch (e) { + console.error('Failed to parse register platform response:', e); + } + } + + return { + success, + status: res.status, + response: result, + tokens: result ? { + accessToken: result.access_token, + refreshToken: result.refresh_token, + uid: result.uid, + } : null, + }; +} + +/** + * 使用帳號密碼登入 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID + * @param {string} options.password - 密碼 + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 登入結果 + */ +export function loginWithCredentials(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'http://localhost:8888', + loginId, + password, + customMetrics = null, + } = options; + + if (!loginId || !password) { + throw new Error('loginId and password are required for credentials login'); + } + + const url = `${baseUrl}/api/v1/auth/sessions`; + const payload = JSON.stringify({ + auth_method: 'credentials', + login_id: loginId, + credentials: { + password: password, + password_confirm: password, + account_type: 'email', + }, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_login_credentials', + api: 'auth', + method: 'login', + auth_type: 'credentials', + }, + }; + + const startTime = Date.now(); + const res = http.post(url, payload, params); + const duration = Date.now() - startTime; + + // 解析響應 + let result = null; + let responseData = null; + + if (res.status === 200) { + try { + result = JSON.parse(res.body); + + // 支持兩種響應格式: + // 1. 直接返回 LoginResp: { access_token, refresh_token, uid, ... } + // 2. 包裝在 Resp 中: { code, message, data: { access_token, ... } } + if (result.data && typeof result.data === 'object') { + // 格式 2: 包裝在 Resp 中 + responseData = result.data; + } else if (result.access_token) { + // 格式 1: 直接返回 LoginResp + responseData = result; + } else { + // 無法識別的格式,記錄響應以便調試 + console.warn('Unexpected login response format. Full response:', JSON.stringify(result)); + console.warn('Response keys:', Object.keys(result)); + responseData = result; + } + } catch (e) { + console.error('Failed to parse login response:', e); + console.error('Response body:', res.body); + } + } + + // 檢查響應結果 + const success = check(res, { + 'login status is 200': (r) => r.status === 200, + 'login has access_token': () => { + return responseData && responseData.access_token && responseData.access_token.length > 0; + }, + 'login has refresh_token': () => { + return responseData && responseData.refresh_token && responseData.refresh_token.length > 0; + }, + }, { name: 'login_checks' }); + + if (customMetrics) { + customMetrics.loginSuccessRate?.add(success); + customMetrics.loginDuration?.add(duration); + } else { + loginSuccessRate.add(success); + loginDuration.add(duration); + } + + return { + success, + status: res.status, + response: result, + responseData: responseData, + tokens: responseData && responseData.access_token ? { + accessToken: responseData.access_token, + refreshToken: responseData.refresh_token, + uid: responseData.uid, + } : null, + }; +} + +/** + * 使用第三方平台登入 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID + * @param {string} options.provider - 平台名稱 + * @param {string} options.token - 平台 Token + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 登入結果 + */ +export function loginWithPlatform(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + loginId, + provider = 'google', + token = 'mock_platform_token', + customMetrics = null, + } = options; + + if (!loginId) { + throw new Error('loginId is required for platform login'); + } + + const url = `${baseUrl}/api/v1/auth/sessions`; + const payload = JSON.stringify({ + auth_method: 'platform', + login_id: loginId, + platform: { + provider: provider, + token: token, + }, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_login_platform', + api: 'auth', + method: 'login', + auth_type: 'platform', + provider: provider, + }, + }; + + const startTime = Date.now(); + const res = http.post(url, payload, params); + const duration = Date.now() - startTime; + + const success = check(res, { + 'login platform status is 200': (r) => r.status === 200, + 'login platform has tokens': (r) => { + try { + const body = JSON.parse(r.body); + return body.access_token && body.refresh_token; + } catch { + return false; + } + }, + }, { name: 'login_platform_checks' }); + + if (customMetrics) { + customMetrics.loginSuccessRate?.add(success); + customMetrics.loginDuration?.add(duration); + } else { + loginSuccessRate.add(success); + loginDuration.add(duration); + } + + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body); + } catch (e) { + console.error('Failed to parse login platform response:', e); + } + } + + return { + success, + status: res.status, + response: result, + tokens: result ? { + accessToken: result.access_token, + refreshToken: result.refresh_token, + uid: result.uid, + } : null, + }; +} + +/** + * 刷新 Access Token + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - 當前的 Access Token + * @param {string} options.refreshToken - Refresh Token + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 刷新結果 + */ +export function refreshToken(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + refreshToken, + customMetrics = null, + } = options; + + if (!accessToken || !refreshToken) { + throw new Error('accessToken and refreshToken are required'); + } + + const url = `${baseUrl}/api/v1/auth/sessions/refresh`; + const payload = JSON.stringify({ + access_token: accessToken, + refresh_token: refreshToken, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_refresh_token', + api: 'auth', + method: 'refresh_token', + }, + }; + + const res = http.post(url, payload, params); + + const success = check(res, { + 'refresh token status is 200': (r) => r.status === 200, + 'refresh token has new access_token': (r) => { + try { + const body = JSON.parse(r.body); + console.log('refresh token response:', body.data); + return body.data.access_token && body.data.access_token.length > 0; + } catch { + return false; + } + }, + }, { name: 'refresh_token_checks' }); + + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body.data); + } catch (e) { + console.error('Failed to parse refresh token response:', e); + } + } + + return { + success, + status: res.status, + response: result, + tokens: result ? { + accessToken: result.access_token, + refreshToken: result.refresh_token, + } : null, + }; +} + +/** + * 請求密碼重設驗證碼 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.identifier - 使用者帳號(email 或 phone) + * @param {string} options.accountType - 帳號類型(email/phone) + * @returns {Object} 請求結果 + */ +export function requestPasswordReset(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'http://localhost:8888', + identifier, + accountType = 'email', + } = options; + + if (!identifier) { + throw new Error('identifier is required'); + } + + const url = `${baseUrl}/api/v1/auth/password-resets/request`; + const payload = JSON.stringify({ + identifier: identifier, + account_type: accountType, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_request_password_reset', + api: 'auth', + method: 'request_password_reset', + }, + }; + + const res = http.post(url, payload, params); + + const success = check(res, { + 'request password reset status is 200': (r) => r.status === 200, + }, { name: 'request_password_reset_checks' }); + + return { + success, + status: res.status, + }; +} + +/** + * 驗證密碼重設驗證碼 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.identifier - 使用者帳號 + * @param {string} options.verifyCode - 驗證碼 + * @returns {Object} 驗證結果 + */ +export function verifyPasswordResetCode(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'http://localhost:8888', + identifier, + verifyCode, + } = options; + + if (!identifier || !verifyCode) { + throw new Error('identifier and verifyCode are required'); + } + + const url = `${baseUrl}/api/v1/auth/password-resets/verify`; + const payload = JSON.stringify({ + identifier: identifier, + verify_code: verifyCode, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_verify_password_reset_code', + api: 'auth', + method: 'verify_password_reset_code', + }, + }; + + const res = http.post(url, payload, params); + + const success = check(res, { + 'verify password reset code status is 200': (r) => r.status === 200, + }, { name: 'verify_password_reset_code_checks' }); + + return { + success, + status: res.status, + }; +} + +/** + * 執行密碼重設 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.identifier - 使用者帳號 + * @param {string} options.verifyCode - 驗證碼 + * @param {string} options.newPassword - 新密碼 + * @returns {Object} 重設結果 + */ +export function resetPassword(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + identifier, + verifyCode, + newPassword, + } = options; + + if (!identifier || !verifyCode || !newPassword) { + throw new Error('identifier, verifyCode, and newPassword are required'); + } + + const url = `${baseUrl}/api/v1/auth/password-resets`; + const payload = JSON.stringify({ + identifier: identifier, + verify_code: verifyCode, + password: newPassword, + password_confirm: newPassword, + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'auth_reset_password', + api: 'auth', + method: 'reset_password', + }, + }; + + const res = http.put(url, payload, params); + + const success = check(res, { + 'reset password status is 200': (r) => r.status === 200, + }, { name: 'reset_password_checks' }); + + return { + success, + status: res.status, + }; +} + diff --git a/test/scenarios/apis/health.js b/test/scenarios/apis/health.js new file mode 100644 index 0000000..326eef5 --- /dev/null +++ b/test/scenarios/apis/health.js @@ -0,0 +1,67 @@ +/** + * 健康檢查 API 場景模組 + * + * 此模組提供系統健康檢查場景,用於監控系統狀態。 + * + * 使用方式: + * import { healthCheck } from './scenarios/apis/health.js'; + */ + +import http from 'k6/http'; +import { check } from 'k6'; +import { Rate, Trend } from 'k6/metrics'; + +// 可選的自定義指標 +const healthCheckSuccessRate = new Rate('health_check_success'); +const healthCheckDuration = new Trend('health_check_duration'); + +/** + * 系統健康檢查 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 健康檢查結果 + */ +export function healthCheck(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'http://localhost:8888', + customMetrics = null, + } = options; + + const url = `${baseUrl}/api/v1/health`; + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + name: 'health_check', + api: 'health', + method: 'health_check', + }, + }; + + const startTime = Date.now(); + const res = http.get(url, params); + const duration = Date.now() - startTime; + + const success = check(res, { + 'health check status is 200': (r) => r.status === 200, + 'health check response time < 500ms': (r) => r.timings.duration < 500, + }, { name: 'health_check_checks' }); + + if (customMetrics) { + customMetrics.healthCheckSuccessRate?.add(success); + customMetrics.healthCheckDuration?.add(duration); + } else { + healthCheckSuccessRate.add(success); + healthCheckDuration.add(duration); + } + + return { + success, + status: res.status, + responseTime: res.timings.duration, + }; +} + diff --git a/test/scenarios/apis/user.js b/test/scenarios/apis/user.js new file mode 100644 index 0000000..eeaa801 --- /dev/null +++ b/test/scenarios/apis/user.js @@ -0,0 +1,341 @@ +/** + * 使用者資訊相關 API 場景模組 + * + * 此模組提供可重複使用的使用者資訊相關場景,包括: + * - 取得使用者資訊 + * - 更新使用者資訊 + * - 修改密碼 + * - 驗證碼流程(email/phone) + * + * 使用方式: + * import { getUserInfo, updateUserInfo } from './scenarios/apis/user.js'; + */ + +import http from 'k6/http'; +import { check } from 'k6'; +import { Rate, Trend } from 'k6/metrics'; + +// 可選的自定義指標 +const getUserInfoSuccessRate = new Rate('user_get_info_success'); +const updateUserInfoSuccessRate = new Rate('user_update_info_success'); +const getUserInfoDuration = new Trend('user_get_info_duration'); +const updateUserInfoDuration = new Trend('user_update_info_duration'); + +/** + * 取得當前登入的使用者資訊 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 使用者資訊結果 + */ +export function getUserInfo(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + customMetrics = null, + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + const url = `${baseUrl}/api/v1/user/me`; + + const params = { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + tags: { + name: 'user_get_info', + api: 'user', + method: 'get_user_info', + }, + }; + + const startTime = Date.now(); + const res = http.get(url, params); + const duration = Date.now() - startTime; + + const success = check(res, { + 'get user info status is 200': (r) => r.status === 200, + 'get user info has uid': (r) => { + try { + const body = JSON.parse(r.body); + return body.uid && body.uid.length > 0; + } catch { + return false; + } + }, + 'get user info has user_status': (r) => { + try { + const body = JSON.parse(r.body); + return body.user_status !== undefined; + } catch { + return false; + } + }, + }, { name: 'get_user_info_checks' }); + + if (customMetrics) { + customMetrics.getUserInfoSuccessRate?.add(success); + customMetrics.getUserInfoDuration?.add(duration); + } else { + getUserInfoSuccessRate.add(success); + getUserInfoDuration.add(duration); + } + + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body); + } catch (e) { + console.error('Failed to parse get user info response:', e); + } + } + + return { + success, + status: res.status, + response: result, + }; +} + +/** + * 更新當前登入的使用者資訊 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {Object} options.updateData - 要更新的資料(可選欄位) + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 更新結果 + */ +export function updateUserInfo(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + updateData = {}, + customMetrics = null, + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + const url = `${baseUrl}/api/v1/user/me`; + const payload = JSON.stringify(updateData); + + const params = { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + tags: { + name: 'user_update_info', + api: 'user', + method: 'update_user_info', + }, + }; + + const startTime = Date.now(); + const res = http.put(url, payload, params); + const duration = Date.now() - startTime; + + const success = check(res, { + 'update user info status is 200': (r) => r.status === 200, + 'update user info returns updated data': (r) => { + try { + const body = JSON.parse(r.body); + return body.uid && body.uid.length > 0; + } catch { + return false; + } + }, + }, { name: 'update_user_info_checks' }); + + if (customMetrics) { + customMetrics.updateUserInfoSuccessRate?.add(success); + customMetrics.updateUserInfoDuration?.add(duration); + } else { + updateUserInfoSuccessRate.add(success); + updateUserInfoDuration.add(duration); + } + + let result = null; + if (res.status === 200) { + try { + result = JSON.parse(res.body); + } catch (e) { + console.error('Failed to parse update user info response:', e); + } + } + + return { + success, + status: res.status, + response: result, + }; +} + +/** + * 修改當前登入使用者的密碼 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {string} options.currentPassword - 當前密碼 + * @param {string} options.newPassword - 新密碼 + * @returns {Object} 修改結果 + */ +export function updatePassword(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + currentPassword, + newPassword, + } = options; + + if (!accessToken || !currentPassword || !newPassword) { + throw new Error('accessToken, currentPassword, and newPassword are required'); + } + + const url = `${baseUrl}/api/v1/user/me/password`; + const payload = JSON.stringify({ + current_password: currentPassword, + new_password: newPassword, + new_password_confirm: newPassword, + }); + + const params = { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + tags: { + name: 'user_update_password', + api: 'user', + method: 'update_password', + }, + }; + + const res = http.put(url, payload, params); + + const success = check(res, { + 'update password status is 200': (r) => r.status === 200, + }, { name: 'update_password_checks' }); + + return { + success, + status: res.status, + }; +} + +/** + * 請求發送驗證碼(用於驗證 email/phone) + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {string} options.purpose - 驗證目的(email_verification/phone_verification) + * @returns {Object} 請求結果 + */ +export function requestVerificationCode(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + purpose = 'email_verification', + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + if (!['email_verification', 'phone_verification'].includes(purpose)) { + throw new Error('purpose must be email_verification or phone_verification'); + } + + const url = `${baseUrl}/api/v1/user/me/verifications`; + const payload = JSON.stringify({ + purpose: purpose, + }); + + const params = { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + tags: { + name: 'user_request_verification_code', + api: 'user', + method: 'request_verification_code', + purpose: purpose, + }, + }; + + const res = http.post(url, payload, params); + + const success = check(res, { + 'request verification code status is 200': (r) => r.status === 200, + }, { name: 'request_verification_code_checks' }); + + return { + success, + status: res.status, + }; +} + +/** + * 提交驗證碼以完成驗證 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {string} options.purpose - 驗證目的(email_verification/phone_verification) + * @param {string} options.verifyCode - 驗證碼 + * @returns {Object} 提交結果 + */ +export function submitVerificationCode(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + purpose = 'email_verification', + verifyCode, + } = options; + + if (!accessToken || !verifyCode) { + throw new Error('accessToken and verifyCode are required'); + } + + if (!['email_verification', 'phone_verification'].includes(purpose)) { + throw new Error('purpose must be email_verification or phone_verification'); + } + + const url = `${baseUrl}/api/v1/user/me/verifications`; + const payload = JSON.stringify({ + purpose: purpose, + verify_code: verifyCode, + }); + + const params = { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + tags: { + name: 'user_submit_verification_code', + api: 'user', + method: 'submit_verification_code', + purpose: purpose, + }, + }; + + const res = http.put(url, payload, params); + + const success = check(res, { + 'submit verification code status is 200': (r) => r.status === 200, + }, { name: 'submit_verification_code_checks' }); + + return { + success, + status: res.status, + }; +} + diff --git a/test/scenarios/e2e/authentication-flow.js b/test/scenarios/e2e/authentication-flow.js new file mode 100644 index 0000000..20361e3 --- /dev/null +++ b/test/scenarios/e2e/authentication-flow.js @@ -0,0 +1,242 @@ +/** + * 認證流程端到端場景 + * + * 此模組提供完整的認證流程場景,組合多個 API 場景形成業務流程。 + * + * 使用方式: + * import { registerAndLoginFlow, loginAndRefreshFlow, passwordResetFlow } from './scenarios/e2e/authentication-flow.js'; + */ + +import * as auth from '../apis/auth.js'; +import { sleep } from 'k6'; + +/** + * 註冊後立即登入流程 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID + * @param {string} options.password - 密碼 + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 流程結果 + */ +export function registerAndLoginFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + loginId = `test_${Date.now()}@example.com`, + password = 'Test123456!', + customMetrics = null, + } = options; + + // 步驟 1: 註冊 + const registerResult = auth.registerWithCredentials({ + baseUrl, + loginId, + password, + customMetrics, + }); + + if (!registerResult.success || !registerResult.tokens) { + return { + success: false, + step: 'register', + error: 'Registration failed', + registerResult, + }; + } + + // 短暫等待,模擬真實用戶行為 + sleep(1); + + // 步驟 2: 使用註冊的帳號登入 + const loginResult = auth.loginWithCredentials({ + baseUrl, + loginId, + password, + customMetrics, + }); + + if (!loginResult.success || !loginResult.tokens) { + return { + success: false, + step: 'login', + error: 'Login failed after registration', + registerResult, + loginResult, + }; + } + + return { + success: true, + registerResult, + loginResult, + tokens: loginResult.tokens, + }; +} + +/** + * 登入後刷新 Token 流程 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID + * @param {string} options.password - 密碼 + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 流程結果 + */ +export function loginAndRefreshFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + loginId, + password, + customMetrics = null, + } = options; + + if (!loginId || !password) { + throw new Error('loginId and password are required'); + } + + // 步驟 1: 登入 + const loginResult = auth.loginWithCredentials({ + baseUrl, + loginId, + password, + customMetrics, + }); + + if (!loginResult.success || !loginResult.tokens) { + return { + success: false, + step: 'login', + error: 'Login failed', + loginResult, + }; + } + + // 短暫等待 + sleep(1); + + // 步驟 2: 刷新 Token + const refreshResult = auth.refreshToken({ + baseUrl, + accessToken: loginResult.tokens.accessToken, + refreshToken: loginResult.tokens.refreshToken, + customMetrics, + }); + + if (!refreshResult.success || !refreshResult.tokens) { + return { + success: false, + step: 'refresh', + error: 'Token refresh failed', + loginResult, + refreshResult, + }; + } + + return { + success: true, + loginResult, + refreshResult, + tokens: refreshResult.tokens, + }; +} + +/** + * 完整密碼重設流程(請求 → 驗證 → 重設) + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.identifier - 使用者帳號(email 或 phone) + * @param {string} options.accountType - 帳號類型(email/phone) + * @param {string} options.verifyCode - 驗證碼(在實際測試中可能需要從外部獲取) + * @param {string} options.newPassword - 新密碼 + * @returns {Object} 流程結果 + */ +export function passwordResetFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + identifier, + accountType = 'email', + verifyCode, + newPassword = 'NewPassword123!', + } = options; + + if (!identifier) { + throw new Error('identifier is required'); + } + + // 步驟 1: 請求密碼重設驗證碼 + const requestResult = auth.requestPasswordReset({ + baseUrl, + identifier, + accountType, + }); + + if (!requestResult.success) { + return { + success: false, + step: 'request', + error: 'Request password reset failed', + requestResult, + }; + } + + // 短暫等待,模擬用戶收到驗證碼的時間 + sleep(2); + + // 步驟 2: 驗證密碼重設驗證碼 + if (!verifyCode) { + // 在實際測試中,驗證碼可能需要從外部獲取(如測試資料庫、郵件服務等) + return { + success: false, + step: 'verify', + error: 'verifyCode is required but not provided', + requestResult, + note: 'In real testing, verifyCode should be retrieved from external source (DB, email service, etc.)', + }; + } + + const verifyResult = auth.verifyPasswordResetCode({ + baseUrl, + identifier, + verifyCode, + }); + + if (!verifyResult.success) { + return { + success: false, + step: 'verify', + error: 'Verify password reset code failed', + requestResult, + verifyResult, + }; + } + + // 短暫等待 + sleep(1); + + // 步驟 3: 執行密碼重設 + const resetResult = auth.resetPassword({ + baseUrl, + identifier, + verifyCode, + newPassword, + }); + + if (!resetResult.success) { + return { + success: false, + step: 'reset', + error: 'Reset password failed', + requestResult, + verifyResult, + resetResult, + }; + } + + return { + success: true, + requestResult, + verifyResult, + resetResult, + }; +} + diff --git a/test/scenarios/e2e/user-profile-flow.js b/test/scenarios/e2e/user-profile-flow.js new file mode 100644 index 0000000..7b306b3 --- /dev/null +++ b/test/scenarios/e2e/user-profile-flow.js @@ -0,0 +1,369 @@ +/** + * 使用者資料管理流程端到端場景 + * + * 此模組提供完整的使用者資料管理流程場景,組合多個 API 場景形成業務流程。 + * + * 使用方式: + * import { getAndUpdateProfileFlow, emailVerificationFlow, phoneVerificationFlow, passwordChangeFlow } from './scenarios/e2e/user-profile-flow.js'; + */ + +import * as auth from '../apis/auth.js'; +import * as user from '../apis/user.js'; +import { sleep } from 'k6'; + +/** + * 取得並更新個人資料流程 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {Object} options.updateData - 要更新的資料 + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 流程結果 + */ +export function getAndUpdateProfileFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + updateData = { + nickname: `TestUser_${Date.now()}`, + preferred_language: 'zh-tw', + }, + customMetrics = null, + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + // 步驟 1: 取得使用者資訊 + const getInfoResult = user.getUserInfo({ + baseUrl, + accessToken, + customMetrics, + }); + + if (!getInfoResult.success) { + return { + success: false, + step: 'get_info', + error: 'Get user info failed', + getInfoResult, + }; + } + + // 短暫等待 + sleep(1); + + // 步驟 2: 更新使用者資訊 + const updateInfoResult = user.updateUserInfo({ + baseUrl, + accessToken, + updateData, + customMetrics, + }); + + if (!updateInfoResult.success) { + return { + success: false, + step: 'update_info', + error: 'Update user info failed', + getInfoResult, + updateInfoResult, + }; + } + + return { + success: true, + getInfoResult, + updateInfoResult, + originalData: getInfoResult.response, + updatedData: updateInfoResult.response, + }; +} + +/** + * 完整 Email 驗證流程(請求 → 提交) + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {string} options.verifyCode - 驗證碼(在實際測試中可能需要從外部獲取) + * @returns {Object} 流程結果 + */ +export function emailVerificationFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + verifyCode, + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + // 步驟 1: 請求 Email 驗證碼 + const requestResult = user.requestVerificationCode({ + baseUrl, + accessToken, + purpose: 'email_verification', + }); + + if (!requestResult.success) { + return { + success: false, + step: 'request', + error: 'Request email verification code failed', + requestResult, + }; + } + + // 短暫等待,模擬用戶收到驗證碼的時間 + sleep(2); + + // 步驟 2: 提交驗證碼 + if (!verifyCode) { + return { + success: false, + step: 'submit', + error: 'verifyCode is required but not provided', + requestResult, + note: 'In real testing, verifyCode should be retrieved from external source (DB, email service, etc.)', + }; + } + + const submitResult = user.submitVerificationCode({ + baseUrl, + accessToken, + purpose: 'email_verification', + verifyCode, + }); + + if (!submitResult.success) { + return { + success: false, + step: 'submit', + error: 'Submit email verification code failed', + requestResult, + submitResult, + }; + } + + return { + success: true, + requestResult, + submitResult, + }; +} + +/** + * 完整手機驗證流程(請求 → 提交) + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {string} options.verifyCode - 驗證碼(在實際測試中可能需要從外部獲取) + * @returns {Object} 流程結果 + */ +export function phoneVerificationFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + verifyCode, + } = options; + + if (!accessToken) { + throw new Error('accessToken is required'); + } + + // 步驟 1: 請求手機驗證碼 + const requestResult = user.requestVerificationCode({ + baseUrl, + accessToken, + purpose: 'phone_verification', + }); + + if (!requestResult.success) { + return { + success: false, + step: 'request', + error: 'Request phone verification code failed', + requestResult, + }; + } + + // 短暫等待,模擬用戶收到驗證碼的時間 + sleep(2); + + // 步驟 2: 提交驗證碼 + if (!verifyCode) { + return { + success: false, + step: 'submit', + error: 'verifyCode is required but not provided', + requestResult, + note: 'In real testing, verifyCode should be retrieved from external source (DB, SMS service, etc.)', + }; + } + + const submitResult = user.submitVerificationCode({ + baseUrl, + accessToken, + purpose: 'phone_verification', + verifyCode, + }); + + if (!submitResult.success) { + return { + success: false, + step: 'submit', + error: 'Submit phone verification code failed', + requestResult, + submitResult, + }; + } + + return { + success: true, + requestResult, + submitResult, + }; +} + +/** + * 登入狀態下修改密碼流程 + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.accessToken - Access Token + * @param {string} options.currentPassword - 當前密碼 + * @param {string} options.newPassword - 新密碼 + * @returns {Object} 流程結果 + */ +export function passwordChangeFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + accessToken, + currentPassword, + newPassword = 'NewPassword123!', + } = options; + + if (!accessToken || !currentPassword) { + throw new Error('accessToken and currentPassword are required'); + } + + // 步驟 1: 修改密碼 + const updatePasswordResult = user.updatePassword({ + baseUrl, + accessToken, + currentPassword, + newPassword, + }); + + if (!updatePasswordResult.success) { + return { + success: false, + step: 'update_password', + error: 'Update password failed', + updatePasswordResult, + }; + } + + // 短暫等待 + sleep(1); + + // 步驟 2: 使用新密碼登入驗證(可選) + // 注意:這需要知道 loginId,可能需要從 getUserInfo 獲取 + // 這裡僅作為示例,實際使用時可能需要調整 + + return { + success: true, + updatePasswordResult, + }; +} + +/** + * 完整的使用者資料初始化流程(註冊 → 登入 → 取得資訊 → 更新資訊) + * @param {Object} options - 配置選項 + * @param {string} options.baseUrl - API 基礎 URL + * @param {string} options.loginId - 登入 ID + * @param {string} options.password - 密碼 + * @param {Object} options.updateData - 要更新的資料 + * @param {Object} options.customMetrics - 自定義指標對象(可選) + * @returns {Object} 流程結果 + */ +export function userProfileInitializationFlow(options = {}) { + const { + baseUrl = __ENV.BASE_URL || 'https://localhost:8888', + loginId = `test_${Date.now()}@example.com`, + password = 'Test123456!', + updateData = { + nickname: `TestUser_${Date.now()}`, + preferred_language: 'zh-tw', + currency: 'TWD', + }, + customMetrics = null, + } = options; + + // 步驟 1: 註冊 + const registerResult = auth.registerWithCredentials({ + baseUrl, + loginId, + password, + customMetrics, + }); + + if (!registerResult.success || !registerResult.tokens) { + return { + success: false, + step: 'register', + error: 'Registration failed', + registerResult, + }; + } + + sleep(1); + + // 步驟 2: 取得使用者資訊 + const getInfoResult = user.getUserInfo({ + baseUrl, + accessToken: registerResult.tokens.accessToken, + customMetrics, + }); + + if (!getInfoResult.success) { + return { + success: false, + step: 'get_info', + error: 'Get user info failed', + registerResult, + getInfoResult, + }; + } + + sleep(1); + + // 步驟 3: 更新使用者資訊 + const updateInfoResult = user.updateUserInfo({ + baseUrl, + accessToken: registerResult.tokens.accessToken, + updateData, + customMetrics, + }); + + if (!updateInfoResult.success) { + return { + success: false, + step: 'update_info', + error: 'Update user info failed', + registerResult, + getInfoResult, + updateInfoResult, + }; + } + + return { + success: true, + registerResult, + getInfoResult, + updateInfoResult, + tokens: registerResult.tokens, + }; +} + diff --git a/test/tests/pre/load-auth-test.js b/test/tests/pre/load-auth-test.js new file mode 100644 index 0000000..093783b --- /dev/null +++ b/test/tests/pre/load-auth-test.js @@ -0,0 +1,69 @@ +/** + * 認證功能負載測試 + * + * 此測試用於 Pre-release 環境,模擬正常負載下的認證功能。 + * 測試重點:系統在正常負載下的性能和穩定性。 + */ + +import { registerWithCredentials, loginWithCredentials, refreshToken } from '../../scenarios/apis/auth.js'; + +export const options = { + scenarios: { + load_auth: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '30s', target: 10 }, // 30 秒內增加到 10 個 VU + { duration: '1m', target: 10 }, // 維持 10 個 VU 1 分鐘 + { duration: '30s', target: 20 }, // 30 秒內增加到 20 個 VU + { duration: '1m', target: 20 }, // 維持 20 個 VU 1 分鐘 + { duration: '30s', target: 0 }, // 30 秒內減少到 0 個 VU + ], + gracefulRampDown: '30s', + tags: { test_type: 'load', api: 'auth', environment: 'pre' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], // 95% 的檢查必須通過 + http_req_duration: ['p(95)<2000'], // 95% 的請求應在 2 秒內完成 + http_req_failed: ['rate<0.05'], // 失敗率應低於 5% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'http://localhost:8888'; + const timestamp = Date.now(); + const randomId = Math.floor(Math.random() * 1000000); + const loginId = `load_test_${timestamp}_${randomId}@example.com`; + const password = 'LoadTest123!'; + + // 1. 註冊 + const registerResult = registerWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!registerResult.success) { + return; + } + + // 2. 登入 + const loginResult = loginWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!loginResult.success || !loginResult.tokens) { + return; + } + + // 3. 刷新 Token + refreshToken({ + baseUrl, + accessToken: loginResult.tokens.accessToken, + refreshToken: loginResult.tokens.refreshToken, + }); +} + diff --git a/test/tests/pre/load-user-profile-flow-test.js b/test/tests/pre/load-user-profile-flow-test.js new file mode 100644 index 0000000..aae197e --- /dev/null +++ b/test/tests/pre/load-user-profile-flow-test.js @@ -0,0 +1,59 @@ +/** + * 使用者資料流程負載測試 + * + * 此測試用於 Pre-release 環境,模擬正常負載下的使用者資料管理流程。 + * 測試重點:系統在正常負載下的流程性能和穩定性。 + */ + +import { userProfileInitializationFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +export const options = { + scenarios: { + load_user_profile_flow: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '30s', target: 5 }, // 30 秒內增加到 5 個 VU + { duration: '1m', target: 5 }, // 維持 5 個 VU 1 分鐘 + { duration: '30s', target: 10 }, // 30 秒內增加到 10 個 VU + { duration: '1m', target: 10 }, // 維持 10 個 VU 1 分鐘 + { duration: '30s', target: 0 }, // 30 秒內減少到 0 個 VU + ], + gracefulRampDown: '30s', + tags: { test_type: 'load', api: 'user', flow: 'profile', environment: 'pre' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], // 95% 的檢查必須通過 + http_req_duration: ['p(95)<3000'], // 95% 的請求應在 3 秒內完成 + http_req_failed: ['rate<0.05'], // 失敗率應低於 5% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + const timestamp = Date.now(); + const randomId = Math.floor(Math.random() * 1000000); + const loginId = `load_flow_${timestamp}_${randomId}@example.com`; + const password = 'LoadTest123!'; + + // 運行完整的使用者資料初始化流程 + const result = userProfileInitializationFlow({ + baseUrl, + loginId, + password, + updateData: { + nickname: `LoadFlow_${timestamp}_${randomId}`, + preferred_language: 'zh-tw', + currency: 'TWD', + }, + }); + + if (!result.success) { + console.error(`Load test failed: ${result.step} - ${result.error}`); + return; + } + + console.log('Load test: User profile flow completed successfully'); +} + diff --git a/test/tests/pre/load-user-test.js b/test/tests/pre/load-user-test.js new file mode 100644 index 0000000..689ca1d --- /dev/null +++ b/test/tests/pre/load-user-test.js @@ -0,0 +1,74 @@ +/** + * 使用者功能負載測試 + * + * 此測試用於 Pre-release 環境,模擬正常負載下的使用者功能。 + * 測試重點:系統在正常負載下的性能和穩定性。 + */ + +import { registerWithCredentials } from '../../scenarios/apis/auth.js'; +import { getUserInfo, updateUserInfo } from '../../scenarios/apis/user.js'; + +export const options = { + scenarios: { + load_user: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '30s', target: 10 }, // 30 秒內增加到 10 個 VU + { duration: '1m', target: 10 }, // 維持 10 個 VU 1 分鐘 + { duration: '30s', target: 20 }, // 30 秒內增加到 20 個 VU + { duration: '1m', target: 20 }, // 維持 20 個 VU 1 分鐘 + { duration: '30s', target: 0 }, // 30 秒內減少到 0 個 VU + ], + gracefulRampDown: '30s', + tags: { test_type: 'load', api: 'user', environment: 'pre' }, + }, + }, + thresholds: { + checks: ['rate>0.95'], // 95% 的檢查必須通過 + http_req_duration: ['p(95)<2000'], // 95% 的請求應在 2 秒內完成 + http_req_failed: ['rate<0.05'], // 失敗率應低於 5% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + const timestamp = Date.now(); + const randomId = Math.floor(Math.random() * 1000000); + const loginId = `load_user_${timestamp}_${randomId}@example.com`; + const password = 'LoadTest123!'; + + // 1. 註冊並獲取 Token + const registerResult = registerWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!registerResult.success || !registerResult.tokens) { + return; + } + + const accessToken = registerResult.tokens.accessToken; + + // 2. 取得使用者資訊 + const getInfoResult = getUserInfo({ + baseUrl, + accessToken, + }); + + if (!getInfoResult.success) { + return; + } + + // 3. 更新使用者資訊 + updateUserInfo({ + baseUrl, + accessToken, + updateData: { + nickname: `LoadTest_${timestamp}_${randomId}`, + preferred_language: 'zh-tw', + }, + }); +} + diff --git a/test/tests/pre/stress-auth-test.js b/test/tests/pre/stress-auth-test.js new file mode 100644 index 0000000..cf56940 --- /dev/null +++ b/test/tests/pre/stress-auth-test.js @@ -0,0 +1,58 @@ +/** + * 認證功能壓力測試 + * + * 此測試用於 Pre-release 環境,模擬高負載下的認證功能。 + * 測試重點:系統在高負載下的穩定性和錯誤處理。 + */ + +import { registerWithCredentials, loginWithCredentials } from '../../scenarios/apis/auth.js'; + +export const options = { + scenarios: { + stress_auth: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '1m', target: 50 }, // 1 分鐘內增加到 50 個 VU + { duration: '2m', target: 50 }, // 維持 50 個 VU 2 分鐘 + { duration: '1m', target: 100 }, // 1 分鐘內增加到 100 個 VU + { duration: '2m', target: 100 }, // 維持 100 個 VU 2 分鐘 + { duration: '1m', target: 0 }, // 1 分鐘內減少到 0 個 VU + ], + gracefulRampDown: '30s', + tags: { test_type: 'stress', api: 'auth', environment: 'pre' }, + }, + }, + thresholds: { + checks: ['rate>0.90'], // 90% 的檢查必須通過(壓力測試允許較低成功率) + http_req_duration: ['p(95)<3000'], // 95% 的請求應在 3 秒內完成 + http_req_failed: ['rate<0.10'], // 失敗率應低於 10% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + const timestamp = Date.now(); + const randomId = Math.floor(Math.random() * 1000000); + const loginId = `stress_test_${timestamp}_${randomId}@example.com`; + const password = 'StressTest123!'; + + // 1. 註冊 + const registerResult = registerWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!registerResult.success) { + return; + } + + // 2. 登入 + loginWithCredentials({ + baseUrl, + loginId, + password, + }); +} + diff --git a/test/tests/pre/stress-user-test.js b/test/tests/pre/stress-user-test.js new file mode 100644 index 0000000..4ca7057 --- /dev/null +++ b/test/tests/pre/stress-user-test.js @@ -0,0 +1,69 @@ +/** + * 使用者功能壓力測試 + * + * 此測試用於 Pre-release 環境,模擬高負載下的使用者功能。 + * 測試重點:系統在高負載下的穩定性和錯誤處理。 + */ + +import { registerWithCredentials } from '../../scenarios/apis/auth.js'; +import { getUserInfo, updateUserInfo } from '../../scenarios/apis/user.js'; + +export const options = { + scenarios: { + stress_user: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '1m', target: 50 }, // 1 分鐘內增加到 50 個 VU + { duration: '2m', target: 50 }, // 維持 50 個 VU 2 分鐘 + { duration: '1m', target: 100 }, // 1 分鐘內增加到 100 個 VU + { duration: '2m', target: 100 }, // 維持 100 個 VU 2 分鐘 + { duration: '1m', target: 0 }, // 1 分鐘內減少到 0 個 VU + ], + gracefulRampDown: '30s', + tags: { test_type: 'stress', api: 'user', environment: 'pre' }, + }, + }, + thresholds: { + checks: ['rate>0.90'], // 90% 的檢查必須通過(壓力測試允許較低成功率) + http_req_duration: ['p(95)<3000'], // 95% 的請求應在 3 秒內完成 + http_req_failed: ['rate<0.10'], // 失敗率應低於 10% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + const timestamp = Date.now(); + const randomId = Math.floor(Math.random() * 1000000); + const loginId = `stress_user_${timestamp}_${randomId}@example.com`; + const password = 'StressTest123!'; + + // 1. 註冊並獲取 Token + const registerResult = registerWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!registerResult.success || !registerResult.tokens) { + return; + } + + const accessToken = registerResult.tokens.accessToken; + + // 2. 取得使用者資訊 + getUserInfo({ + baseUrl, + accessToken, + }); + + // 3. 更新使用者資訊 + updateUserInfo({ + baseUrl, + accessToken, + updateData: { + nickname: `StressTest_${timestamp}_${randomId}`, + }, + }); +} + diff --git a/test/tests/prod/nightly-auth-test.js b/test/tests/prod/nightly-auth-test.js new file mode 100644 index 0000000..2a8bb5a --- /dev/null +++ b/test/tests/prod/nightly-auth-test.js @@ -0,0 +1,60 @@ +/** + * 認證功能夜間測試 + * + * 此測試用於 Production 環境,在夜間低峰時段執行。 + * 測試重點:監控生產環境的認證功能穩定性,識別長期性能變化。 + */ + +import { loginWithCredentials, refreshToken } from '../../scenarios/apis/auth.js'; + +export const options = { + scenarios: { + nightly_auth: { + executor: 'constant-vus', + vus: 5, // 低並發,避免影響生產環境 + duration: '5m', // 執行 5 分鐘 + tags: { test_type: 'nightly', api: 'auth', environment: 'prod' }, + }, + }, + thresholds: { + checks: ['rate>0.98'], // 98% 的檢查必須通過(生產環境要求更高) + http_req_duration: ['p(95)<2000'], // 95% 的請求應在 2 秒內完成 + http_req_failed: ['rate<0.02'], // 失敗率應低於 2% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + + // 注意:生產環境測試應使用預先創建的測試帳號 + // 不要創建新帳號,避免污染生產資料 + const loginId = __ENV.TEST_LOGIN_ID || 'test@example.com'; + const password = __ENV.TEST_PASSWORD || 'TestPassword123!'; + + // 1. 登入(使用預先創建的測試帳號) + const loginResult = loginWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!loginResult.success || !loginResult.tokens) { + console.error('Nightly test failed: Login failed'); + return; + } + + // 2. 刷新 Token + const refreshResult = refreshToken({ + baseUrl, + accessToken: loginResult.tokens.accessToken, + refreshToken: loginResult.tokens.refreshToken, + }); + + if (!refreshResult.success) { + console.error('Nightly test failed: Token refresh failed'); + return; + } + + console.log('Nightly test passed: Auth operations succeeded'); +} + diff --git a/test/tests/prod/nightly-health-test.js b/test/tests/prod/nightly-health-test.js new file mode 100644 index 0000000..8b2b304 --- /dev/null +++ b/test/tests/prod/nightly-health-test.js @@ -0,0 +1,43 @@ +/** + * 健康檢查夜間監控 + * + * 此測試用於 Production 環境,持續監控系統健康狀態。 + * 測試重點:系統可用性監控,識別性能退化。 + */ + +import { healthCheck } from '../../scenarios/apis/health.js'; + +export const options = { + scenarios: { + nightly_health: { + executor: 'constant-arrival-rate', + rate: 1, // 每秒 1 個請求 + timeUnit: '1s', + duration: '10m', // 執行 10 分鐘 + preAllocatedVUs: 2, + maxVUs: 5, + tags: { test_type: 'nightly', api: 'health', environment: 'prod' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], // 所有檢查必須通過 + http_req_duration: ['p(95)<500'], // 95% 的請求應在 500ms 內完成 + health_check_success: ['rate==1.0'], // 健康檢查成功率應為 100% + http_req_failed: ['rate==0'], // 不允許失敗 + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'http://localhost:8888'; + + const result = healthCheck({ baseUrl }); + + if (!result.success) { + console.error(`Health check failed: Status ${result.status}, Response time ${result.responseTime}ms`); + return; + } + + // 記錄健康檢查結果(在實際環境中,這可以發送到監控系統) + console.log(`Health check passed: Status ${result.status}, Response time ${result.responseTime}ms`); +} + diff --git a/test/tests/prod/nightly-user-test.js b/test/tests/prod/nightly-user-test.js new file mode 100644 index 0000000..4ef58b2 --- /dev/null +++ b/test/tests/prod/nightly-user-test.js @@ -0,0 +1,76 @@ +/** + * 使用者功能夜間測試 + * + * 此測試用於 Production 環境,在夜間低峰時段執行。 + * 測試重點:監控生產環境的使用者功能穩定性,識別長期性能變化。 + */ + +import { loginWithCredentials } from '../../scenarios/apis/auth.js'; +import { getUserInfo, updateUserInfo } from '../../scenarios/apis/user.js'; + +export const options = { + scenarios: { + nightly_user: { + executor: 'constant-vus', + vus: 5, // 低並發,避免影響生產環境 + duration: '5m', // 執行 5 分鐘 + tags: { test_type: 'nightly', api: 'user', environment: 'prod' }, + }, + }, + thresholds: { + checks: ['rate>0.98'], // 98% 的檢查必須通過(生產環境要求更高) + http_req_duration: ['p(95)<2000'], // 95% 的請求應在 2 秒內完成 + http_req_failed: ['rate<0.02'], // 失敗率應低於 2% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + + // 注意:生產環境測試應使用預先創建的測試帳號 + // 不要創建新帳號,避免污染生產資料 + const loginId = __ENV.TEST_LOGIN_ID || 'test@example.com'; + const password = __ENV.TEST_PASSWORD || 'TestPassword123!'; + + // 1. 登入(使用預先創建的測試帳號) + const loginResult = loginWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!loginResult.success || !loginResult.tokens) { + console.error('Nightly test failed: Login failed'); + return; + } + + const accessToken = loginResult.tokens.accessToken; + + // 2. 取得使用者資訊 + const getInfoResult = getUserInfo({ + baseUrl, + accessToken, + }); + + if (!getInfoResult.success) { + console.error('Nightly test failed: Get user info failed'); + return; + } + + // 3. 更新使用者資訊(僅更新非關鍵欄位,避免影響測試帳號) + const updateInfoResult = updateUserInfo({ + baseUrl, + accessToken, + updateData: { + nickname: `NightlyTest_${Date.now()}`, + }, + }); + + if (!updateInfoResult.success) { + console.error('Nightly test failed: Update user info failed'); + return; + } + + console.log('Nightly test passed: User operations succeeded'); +} + diff --git a/test/tests/smoke/smoke-auth-test.js b/test/tests/smoke/smoke-auth-test.js new file mode 100644 index 0000000..c96f65d --- /dev/null +++ b/test/tests/smoke/smoke-auth-test.js @@ -0,0 +1,76 @@ +/** + * 認證功能冒煙測試 + * + * 此測試用於 Dev/QA 環境,快速驗證認證功能是否正常運作。 + * 測試重點:基本功能可用性,不關注性能。 + */ + +import { registerWithCredentials, loginWithCredentials, refreshToken } from '../../scenarios/apis/auth.js'; +import { healthCheck } from '../../scenarios/apis/health.js'; + +export const options = { + scenarios: { + smoke_auth: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '30s', + tags: { test_type: 'smoke', api: 'auth' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], // 所有檢查必須通過 + http_req_duration: ['p(95)<2000'], // 95% 的請求應在 2 秒內完成 + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'http://localhost:8888'; + const timestamp = Date.now(); + const loginId = `smoke_test_${timestamp}@example.com`; + const password = 'SmokeTest123!'; + + // 1. 健康檢查 + healthCheck({ baseUrl }); + + // 2. 註冊 + const registerResult = registerWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!registerResult.success) { + console.error('Smoke test failed: Registration failed'); + return; + } + + // 3. 登入 + const loginResult = loginWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!loginResult.success) { + console.error('Smoke test failed: Login failed'); + return; + } + + // 4. 刷新 Token + if (loginResult.tokens) { + const refreshResult = refreshToken({ + baseUrl, + accessToken: loginResult.tokens.accessToken, + refreshToken: loginResult.tokens.refreshToken, + }); + + if (!refreshResult.success) { + console.error('Smoke test failed: Token refresh failed'); + return; + } + } + + console.log('Smoke test passed: All auth operations succeeded'); +} + diff --git a/test/tests/smoke/smoke-health-test.js b/test/tests/smoke/smoke-health-test.js new file mode 100644 index 0000000..f331d2a --- /dev/null +++ b/test/tests/smoke/smoke-health-test.js @@ -0,0 +1,39 @@ +/** + * 健康檢查冒煙測試 + * + * 此測試用於 Dev/QA 環境,快速驗證系統健康狀態。 + * 測試重點:系統可用性,響應時間。 + */ + +import { healthCheck } from '../../scenarios/apis/health.js'; + +export const options = { + scenarios: { + smoke_health: { + executor: 'shared-iterations', + vus: 1, + iterations: 5, // 執行 5 次健康檢查 + maxDuration: '10s', + tags: { test_type: 'smoke', api: 'health' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], // 所有檢查必須通過 + http_req_duration: ['p(95)<500'], // 95% 的請求應在 500ms 內完成 + health_check_success: ['rate==1.0'], // 健康檢查成功率應為 100% + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'http://localhost:8888'; + + const result = healthCheck({ baseUrl }); + + if (!result.success) { + console.error('Smoke test failed: Health check failed'); + return; + } + + console.log(`Health check passed: Status ${result.status}, Response time ${result.responseTime}ms`); +} + diff --git a/test/tests/smoke/smoke-user-profile-flow-test.js b/test/tests/smoke/smoke-user-profile-flow-test.js new file mode 100644 index 0000000..dbb5ca4 --- /dev/null +++ b/test/tests/smoke/smoke-user-profile-flow-test.js @@ -0,0 +1,75 @@ +/** + * 使用者資料流程冒煙測試 + * + * 此測試用於 Dev/QA 環境,快速驗證使用者資料管理流程是否正常運作。 + * 測試重點:完整流程可用性,不關注性能。 + */ + +import { registerWithCredentials } from '../../scenarios/apis/auth.js'; +import { getAndUpdateProfileFlow, userProfileInitializationFlow } from '../../scenarios/e2e/user-profile-flow.js'; + +export const options = { + scenarios: { + smoke_user_profile_flow: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '30s', + tags: { test_type: 'smoke', api: 'user', flow: 'profile' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], // 所有檢查必須通過 + http_req_duration: ['p(95)<3000'], // 95% 的請求應在 3 秒內完成(流程測試允許稍長) + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'http://localhost:8888'; + const timestamp = Date.now(); + const loginId = `smoke_flow_${timestamp}@example.com`; + const password = 'SmokeTest123!'; + + // 方式 1: 使用完整初始化流程(註冊 → 取得資訊 → 更新資訊) + console.log('測試完整使用者資料初始化流程...'); + const initFlowResult = userProfileInitializationFlow({ + baseUrl, + loginId, + password, + updateData: { + nickname: `SmokeFlow_${timestamp}`, + preferred_language: 'zh-tw', + currency: 'TWD', + }, + }); + + if (!initFlowResult.success) { + console.error(`Smoke test failed: ${initFlowResult.step} - ${initFlowResult.error}`); + return; + } + + console.log('完整初始化流程測試通過'); + + // 方式 2: 使用取得並更新流程(需要先有 Token) + // 這裡使用上面流程獲得的 Token + if (initFlowResult.tokens) { + console.log('測試取得並更新流程...'); + const getUpdateFlowResult = getAndUpdateProfileFlow({ + baseUrl, + accessToken: initFlowResult.tokens.accessToken, + updateData: { + nickname: `SmokeFlow_Updated_${timestamp}`, + preferred_language: 'en-us', + }, + }); + + if (!getUpdateFlowResult.success) { + console.error(`Smoke test failed: ${getUpdateFlowResult.step} - ${getUpdateFlowResult.error}`); + return; + } + + console.log('取得並更新流程測試通過'); + } + + console.log('Smoke test passed: All user profile flow operations succeeded'); +} diff --git a/test/tests/smoke/smoke-user-test.js b/test/tests/smoke/smoke-user-test.js new file mode 100644 index 0000000..6b81e36 --- /dev/null +++ b/test/tests/smoke/smoke-user-test.js @@ -0,0 +1,75 @@ +/** + * 使用者功能冒煙測試 + * + * 此測試用於 Dev/QA 環境,快速驗證使用者功能是否正常運作。 + * 測試重點:基本功能可用性,不關注性能。 + */ + +import { registerWithCredentials, loginWithCredentials } from '../../scenarios/apis/auth.js'; +import { getUserInfo, updateUserInfo } from '../../scenarios/apis/user.js'; + +export const options = { + scenarios: { + smoke_user: { + executor: 'shared-iterations', + vus: 1, + iterations: 1, + maxDuration: '30s', + tags: { test_type: 'smoke', api: 'user' }, + }, + }, + thresholds: { + checks: ['rate==1.0'], // 所有檢查必須通過 + http_req_duration: ['p(95)<2000'], // 95% 的請求應在 2 秒內完成 + }, +}; + +export default function () { + const baseUrl = __ENV.BASE_URL || 'https://localhost:8888'; + const timestamp = Date.now(); + const loginId = `smoke_user_${timestamp}@example.com`; + const password = 'SmokeTest123!'; + + // 1. 註冊並登入以獲取 Token + const registerResult = registerWithCredentials({ + baseUrl, + loginId, + password, + }); + + if (!registerResult.success || !registerResult.tokens) { + console.error('Smoke test failed: Registration failed'); + return; + } + + const accessToken = registerResult.tokens.accessToken; + + // 2. 取得使用者資訊 + const getInfoResult = getUserInfo({ + baseUrl, + accessToken, + }); + + if (!getInfoResult.success) { + console.error('Smoke test failed: Get user info failed'); + return; + } + + // 3. 更新使用者資訊 + const updateInfoResult = updateUserInfo({ + baseUrl, + accessToken, + updateData: { + nickname: `SmokeTest_${timestamp}`, + preferred_language: 'zh-tw', + }, + }); + + if (!updateInfoResult.success) { + console.error('Smoke test failed: Update user info failed'); + return; + } + + console.log('Smoke test passed: All user operations succeeded'); +} +