diff --git a/.dockerignore b/.dockerignore index 038456e..69a1767 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,7 @@ /.git/ +healthcheck +aye-and-nay +coverage.txt /badger/ /build/badger/ /build/caddy/ diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 1d601a0..f614660 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -7,13 +7,17 @@ jobs: integration: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: - go-version: 1.17 - - uses: actions/cache@v2 + go-version: 1.19 + - uses: actions/cache@v3 with: - path: ~/go/pkg/mod + # Module download cache + # Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go- - run: make dev-up @@ -21,7 +25,7 @@ jobs: CONTINUOUS_INTEGRATION: "true" run: make test-int-ci - run: make dev-down - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.txt + files: ./coverage.txt diff --git a/.github/workflows/loadtest.yml b/.github/workflows/loadtest.yml index abf525b..5971524 100644 --- a/.github/workflows/loadtest.yml +++ b/.github/workflows/loadtest.yml @@ -7,13 +7,17 @@ jobs: loadtest: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: - go-version: 1.17 - - uses: actions/cache@v2 + go-version: 1.19 + - uses: actions/cache@v3 with: - path: ~/go/pkg/mod + # Module download cache + # Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go- - run: make prod-up diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index dba8e0a..fe8a813 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -7,17 +7,21 @@ jobs: unit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: - go-version: 1.17 - - uses: actions/cache@v2 + go-version: 1.19 + - uses: actions/cache@v3 with: - path: ~/go/pkg/mod + # Module download cache + # Build cache (Linux) + path: | + ~/go/pkg/mod + ~/.cache/go-build key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go- - run: make test-unit-ci - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.txt + files: ./coverage.txt diff --git a/.gitignore b/.gitignore index 4aa0a88..da553cd 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ *.test # aye-and-nay -main +healthcheck aye-and-nay coverage.txt /badger/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..882fc3d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.analysis.extraPaths": [ + "/Users/mercury/.vscode/extensions/continue.continue-0.0.412-darwin-arm64" + ], + "python.autoComplete.extraPaths": [ + "/Users/mercury/.vscode/extensions/continue.continue-0.0.412-darwin-arm64" + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index efccf7c..207269e 100644 --- a/Makefile +++ b/Makefile @@ -1,45 +1,48 @@ -.PHONY: gen compile test-unit test-int test-unit-ci test-int-ci dev-up dev-down prod-loadtest prod-up prod-down embed-loadtest embed-up embed-down +.PHONY: gen compile compile-health test-unit test-int test-unit-ci test-int-ci dev-up dev-down prod-loadtest prod-up prod-down embed-loadtest embed-up embed-down gen: - go install github.com/mailru/easyjson/easyjson@latest - go install golang.org/x/tools/cmd/stringer@latest + go install github.com/mailru/easyjson/easyjson + go install golang.org/x/tools/cmd/stringer go generate ./... compile: gen CGO_ENABLED=0 go build -ldflags="-s -w" +compile-health: + CGO_ENABLED=0 go build -ldflags="-s -w" -o healthcheck ./cmd/healthcheck/main.go + test-unit: gen - go test -v -race -shuffle=on -count=1 -short -tags=unit -cover ./... + go test -v -race -shuffle=on -count=2 -short -cover ./... -args -unit test-int: gen - go test -v -race -shuffle=on -count=1 -short -tags=integration -cover ./... + go test -v -race -shuffle=on -count=2 -short -cover ./... -args -int test-unit-ci: gen - go test -v -race -shuffle=on -count=1 -tags=unit -failfast -coverprofile=coverage.txt -covermode=atomic ./... + go test -v -race -shuffle=on -count=2 -failfast -coverprofile=coverage.txt -covermode=atomic ./... -args -unit -ci test-int-ci: gen - go test -v -race -shuffle=on -count=1 -tags=integration -failfast -coverprofile=coverage.txt -covermode=atomic ./... + go test -v -race -shuffle=on -count=2 -failfast -coverprofile=coverage.txt -covermode=atomic ./... -args -int -ci dev-up: - docker-compose --file ./build/docker-compose-dev.yml up -d --build + docker compose --file ./build/docker-compose-dev.yml up -d --build dev-down: - docker-compose --file ./build/docker-compose-dev.yml down --rmi all -v + docker compose --file ./build/docker-compose-dev.yml down --rmi all -v prod-loadtest: - go run ./cmd/loadtest/main.go -verbose=false + go run ./cmd/loadtest/* -verbose=false prod-up: - docker-compose --file ./build/docker-compose-prod.yml up -d --build + docker compose --file ./build/docker-compose-prod.yml --env-file ./build/config-prod.env up -d --build prod-down: - docker-compose --file ./build/docker-compose-prod.yml down --rmi all -v + docker compose --file ./build/docker-compose-prod.yml --env-file ./build/config-prod.env down --rmi all -v embed-loadtest: - go run ./cmd/loadtest/main.go -verbose=false -api-address "http://localhost:8001" -minio-address "" + go run ./cmd/loadtest/* -verbose=false -api-address="http://localhost:8001" -html=false embed-up: - docker-compose --file ./build/docker-compose-embed.yml up -d --build + docker compose --file ./build/docker-compose-embed.yml --env-file ./build/config-embed.env up -d --build embed-down: - docker-compose --file ./build/docker-compose-embed.yml down --rmi all -v + docker compose --file ./build/docker-compose-embed.yml --env-file ./build/config-embed.env down --rmi all -v diff --git a/build/Caddyfile b/build/Caddyfile index 4a45a3a..84615e6 100644 --- a/build/Caddyfile +++ b/build/Caddyfile @@ -18,3 +18,7 @@ localhost { # www.aye-and-nay.de { # redir https://aye-and-nay.de{uri} permanent # } + +:8080 { + respond /health 200 +} diff --git a/build/Dockerfile-app b/build/Dockerfile-app index 46cda98..96c4fee 100644 --- a/build/Dockerfile-app +++ b/build/Dockerfile-app @@ -1,15 +1,22 @@ -FROM golang:1.17-alpine AS builder +FROM golang:1.19-alpine AS builder RUN apk add --no-cache make RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app/ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN make compile +RUN make compile compile-health FROM scratch COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /etc/passwd /etc/ COPY --from=builder /app/aye-and-nay / +COPY --from=builder /app/config.env / +COPY --from=builder /app/healthcheck / USER appuser ENTRYPOINT ["/aye-and-nay"] +HEALTHCHECK \ + --interval=1m \ + --timeout=30s \ + --retries=3 \ + CMD ["/healthcheck"] diff --git a/build/Dockerfile-caddy b/build/Dockerfile-caddy index 984a644..9dfd962 100644 --- a/build/Dockerfile-caddy +++ b/build/Dockerfile-caddy @@ -1 +1,7 @@ FROM caddy:2-alpine +COPY ./build/Caddyfile /etc/caddy/Caddyfile +HEALTHCHECK \ + --interval=1m \ + --timeout=30s \ + --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 diff --git a/build/Dockerfile-imaginary b/build/Dockerfile-imaginary index db91e5c..04da83b 100644 --- a/build/Dockerfile-imaginary +++ b/build/Dockerfile-imaginary @@ -1,2 +1,15 @@ FROM h2non/imaginary:1 +USER root +RUN \ + apt-get update && \ + apt-get install --no-install-recommends --yes curl && \ + apt-get autoremove --yes && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* +USER nobody ENV PORT=9001 +HEALTHCHECK \ + --interval=1m \ + --timeout=30s \ + --retries=3 \ + CMD curl -f http://localhost:9001/health || exit 1 diff --git a/build/Dockerfile-minio b/build/Dockerfile-minio index 46816c4..1704de2 100644 --- a/build/Dockerfile-minio +++ b/build/Dockerfile-minio @@ -1,3 +1,8 @@ FROM minio/minio:RELEASE.2021-11-24T23-19-33Z ENV MINIO_ROOT_USER=12345678 MINIO_ROOT_PASSWORD=qwertyui CMD ["server", "/data", "--console-address", ":9090"] +HEALTHCHECK \ + --interval=1m \ + --timeout=30s \ + --retries=3 \ + CMD curl -f http://localhost:9000/minio/health/live || exit 1 diff --git a/build/Dockerfile-mongo b/build/Dockerfile-mongo index 2c06b63..78b5255 100644 --- a/build/Dockerfile-mongo +++ b/build/Dockerfile-mongo @@ -1,2 +1,8 @@ FROM mongo:5 +COPY ./build/mongo.js /docker-entrypoint-initdb.d/mongo.js ENV MONGO_INITDB_DATABASE=aye-and-nay +HEALTHCHECK \ + --interval=1m \ + --timeout=30s \ + --retries=3 \ + CMD test $(echo 'db.runCommand("ping").ok' | mongo --quiet) -eq 1 || exit 1 diff --git a/build/Dockerfile-redis b/build/Dockerfile-redis index e25ea9c..f301408 100644 --- a/build/Dockerfile-redis +++ b/build/Dockerfile-redis @@ -1,2 +1,8 @@ FROM redis:6-alpine +COPY ./build/redis.conf /usr/local/etc/redis/redis.conf CMD ["redis-server", "/usr/local/etc/redis/redis.conf"] +HEALTHCHECK \ + --interval=1m \ + --timeout=30s \ + --retries=3 \ + CMD test $(redis-cli ping) = "PONG" || exit 1 diff --git a/build/Dockerfile-swagger b/build/Dockerfile-swagger index 681dd8b..1f033f8 100644 --- a/build/Dockerfile-swagger +++ b/build/Dockerfile-swagger @@ -1 +1,3 @@ FROM swaggerapi/swagger-ui:v4.1.2 +COPY ./build/swagger.yml /swagger.yml +ENV SWAGGER_JSON=/swagger.yml diff --git a/build/config-dev.env b/build/config-dev.env new file mode 100644 index 0000000..c30218a --- /dev/null +++ b/build/config-dev.env @@ -0,0 +1,93 @@ +# CONFIG +CONFIG_RELOAD=true +CONFIG_RELOAD_INTERVAL=1s + +# APP +APP_NAME=config-dev +APP_LOG=info +APP_GC_TUNER=custom # [none, custom, go] +APP_MEM_TOTAL=2147483648 # 2 GB +APP_MEM_LIMIT_RATIO=0.7 + +# SERVER +SERVER_DOMAIN= +SERVER_HOST=localhost +SERVER_PORT=8001 +SERVER_H2C=false +SERVER_READ_TIMEOUT=60s +SERVER_WRITE_TIMEOUT=120s +SERVER_IDLE_TIMEOUT=120s +SERVER_SHUTDOWN_TIMEOUT=10s + +# MIDDLEWARE +MIDDLEWARE_CORS_ALLOW_ORIGIN=* +MIDDLEWARE_LIMITER_REQUESTS_PER_SECOND=30000 +MIDDLEWARE_LIMITER_BURST=300 +MIDDLEWARE_DEBUG=true + +# CONTROLLER +CONTROLLER_MAX_NUMBER_OF_FILES=100 +CONTROLLER_MAX_FILE_SIZE=5242880 # 5 MB + +# SERVICE +SERVICE_TEMP_LINKS=true +SERVICE_NUMBER_OF_WORKERS_CALC=8 +SERVICE_NUMBER_OF_WORKERS_COMP=8 +SERVICE_ACCURACY=0.625 + +# CACHE: [mem, redis] +APP_CACHE=redis +CACHE_MEM_CLEANUP_INTERVAL=15m +CACHE_REDIS_HOST=localhost +CACHE_REDIS_PORT=6379 +CACHE_REDIS_RETRY_TIMES=4 +CACHE_REDIS_RETRY_PAUSE=5s +CACHE_REDIS_TIMEOUT=30s +CACHE_REDIS_TIME_TO_LIVE=15m +CACHE_REDIS_TX_RETRIES=5 + +# COMPRESSOR: [mock, shortpixel, imaginary] +APP_COMPRESSOR=mock +COMPRESSOR_SHORTPIXEL_URL=https://api.shortpixel.com/v2/post-reducer.php +COMPRESSOR_SHORTPIXEL_URL2=https://api.shortpixel.com/v2/reducer.php +COMPRESSOR_SHORTPIXEL_API_KEY=abcdefghijklmnopqrst +COMPRESSOR_SHORTPIXEL_RETRY_TIMES=2 +COMPRESSOR_SHORTPIXEL_RETRY_PAUSE=10s +COMPRESSOR_SHORTPIXEL_TIMEOUT=30s +COMPRESSOR_SHORTPIXEL_WAIT=30 +COMPRESSOR_SHORTPIXEL_UPLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_DOWNLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_REPEAT_IN=10s +COMPRESSOR_SHORTPIXEL_RESTART_IN=15m +COMPRESSOR_IMAGINARY_HOST=localhost +COMPRESSOR_IMAGINARY_PORT=9001 +COMPRESSOR_IMAGINARY_RETRY_TIMES=4 +COMPRESSOR_IMAGINARY_RETRY_PAUSE=5s +COMPRESSOR_IMAGINARY_TIMEOUT=30s + +# DATABASE: [mem, mongo, badger] +APP_DATABASE=mongo +DATABASE_MONGO_HOST=localhost +DATABASE_MONGO_PORT=27017 +DATABASE_MONGO_RETRY_TIMES=4 +DATABASE_MONGO_RETRY_PAUSE=5s +DATABASE_MONGO_TIMEOUT=30s +DATABASE_MONGO_LRU=100 +DATABASE_BADGER_IN_MEMORY=false +DATABASE_BADGER_GC_RATIO=0.7 +DATABASE_BADGER_CLEANUP_INTERVAL=5m +DATABASE_BADGER_LRU=100 + +# STORAGE: [mock, minio] +APP_STORAGE=minio +STORAGE_MINIO_HOST=localhost +STORAGE_MINIO_PORT=9000 +STORAGE_MINIO_ACCESS_KEY=12345678 +STORAGE_MINIO_SECRET_KEY=qwertyui +STORAGE_MINIO_TOKEN= +STORAGE_MINIO_SECURE=false +STORAGE_MINIO_RETRY_TIMES=4 +STORAGE_MINIO_RETRY_PAUSE=5s +STORAGE_MINIO_TIMEOUT=30s +STORAGE_MINIO_LOCATION=eu-central-1 +STORAGE_MINIO_PREFIX= diff --git a/build/config-dev.yml b/build/config-dev.yml deleted file mode 100644 index 315a254..0000000 --- a/build/config-dev.yml +++ /dev/null @@ -1,87 +0,0 @@ -app: - ballast: 0 - log: info -server: - domain: - host: localhost - port: 8001 - h2c: false - readTimeout: 60s - writeTimeout: 120s - idleTimeout: 120s - shutdownTimeout: 10s -middleware: - cors: - allowOrigin: "*" - limiter: - requestsPerSecond: 10000 - burst: 10 -controller: - maxNumberOfFiles: 100 - maxFileSize: 5242880 # 5 MB -service: - numberOfWorkersCalc: 8 - numberOfWorkersComp: 8 - accuracy: 0.625 -cache: - use: redis - redis: - host: localhost - port: 6379 - retry: - times: 4 - pause: 5s - timeout: 30s - timeToLive: 15m - cleanupInterval: 15m -compressor: - use: imaginary - imaginary: - host: localhost - port: 9001 - retry: - times: 4 - pause: 5s - timeout: 30s - shortpixel: - url: https://api.shortpixel.com/v2/post-reducer.php - url2: https://api.shortpixel.com/v2/reducer.php - apiKey: abcdefghijklmnopqrst - retry: - times: 2 - pause: 10s - timeout: 30s - wait: 30 - uploadTimeout: 60s - downloadTimeout: 60s - repeatIn: 10s - restartIn: 15m -database: - use: mongo - mongo: - host: localhost - port: 27017 - retry: - times: 4 - pause: 5s - timeout: 30s - lru: 100 - badger: - gcRatio: 0.7 - cleanupInterval: 5m - lru: 100 -storage: - use: minio - minio: - host: localhost - port: 9000 - accessKey: 12345678 - secretKey: qwertyui - token: - secure: false - retry: - times: 4 - pause: 5s - timeout: 30s - location: eu-central-1 - prefix: http://localhost:9000 diff --git a/build/config-embed.env b/build/config-embed.env new file mode 100644 index 0000000..f5d592f --- /dev/null +++ b/build/config-embed.env @@ -0,0 +1,93 @@ +# CONFIG +CONFIG_RELOAD=true +CONFIG_RELOAD_INTERVAL=1s + +# APP +APP_NAME=config-embed +APP_LOG=info +APP_GC_TUNER=custom # [none, custom, go] +APP_MEM_TOTAL=2147483648 +APP_MEM_LIMIT_RATIO=0.7 + +# SERVER +SERVER_DOMAIN= +SERVER_HOST= +SERVER_PORT=8001 +SERVER_H2C=false +SERVER_READ_TIMEOUT=60s +SERVER_WRITE_TIMEOUT=120s +SERVER_IDLE_TIMEOUT=120s +SERVER_SHUTDOWN_TIMEOUT=10s + +# MIDDLEWARE +MIDDLEWARE_CORS_ALLOW_ORIGIN=* +MIDDLEWARE_LIMITER_REQUESTS_PER_SECOND=30000 +MIDDLEWARE_LIMITER_BURST=300 +MIDDLEWARE_DEBUG=false + +# CONTROLLER +CONTROLLER_MAX_NUMBER_OF_FILES=100 +CONTROLLER_MAX_FILE_SIZE=5242880 # 5 MB + +# SERVICE +SERVICE_TEMP_LINKS=true +SERVICE_NUMBER_OF_WORKERS_CALC=8 +SERVICE_NUMBER_OF_WORKERS_COMP=8 +SERVICE_ACCURACY=0.625 + +# CACHE: [mem, redis] +APP_CACHE=mem +CACHE_MEM_CLEANUP_INTERVAL=15m +CACHE_REDIS_HOST=localhost +CACHE_REDIS_PORT=6379 +CACHE_REDIS_RETRY_TIMES=4 +CACHE_REDIS_RETRY_PAUSE=5s +CACHE_REDIS_TIMEOUT=30s +CACHE_REDIS_TIME_TO_LIVE=15m +CACHE_REDIS_TX_RETRIES=5 + +# COMPRESSOR: [mock, shortpixel, imaginary] +APP_COMPRESSOR=mock +COMPRESSOR_SHORTPIXEL_URL=https://api.shortpixel.com/v2/post-reducer.php +COMPRESSOR_SHORTPIXEL_URL2=https://api.shortpixel.com/v2/reducer.php +COMPRESSOR_SHORTPIXEL_API_KEY=abcdefghijklmnopqrst +COMPRESSOR_SHORTPIXEL_RETRY_TIMES=2 +COMPRESSOR_SHORTPIXEL_RETRY_PAUSE=10s +COMPRESSOR_SHORTPIXEL_TIMEOUT=30s +COMPRESSOR_SHORTPIXEL_WAIT=30 +COMPRESSOR_SHORTPIXEL_UPLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_DOWNLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_REPEAT_IN=10s +COMPRESSOR_SHORTPIXEL_RESTART_IN=15m +COMPRESSOR_IMAGINARY_HOST=localhost +COMPRESSOR_IMAGINARY_PORT=9001 +COMPRESSOR_IMAGINARY_RETRY_TIMES=4 +COMPRESSOR_IMAGINARY_RETRY_PAUSE=5s +COMPRESSOR_IMAGINARY_TIMEOUT=30s + +# DATABASE: [mem, mongo, badger] +APP_DATABASE=badger +DATABASE_MONGO_HOST=localhost +DATABASE_MONGO_PORT=27017 +DATABASE_MONGO_RETRY_TIMES=4 +DATABASE_MONGO_RETRY_PAUSE=5s +DATABASE_MONGO_TIMEOUT=30s +DATABASE_MONGO_LRU=100 +DATABASE_BADGER_IN_MEMORY=false +DATABASE_BADGER_GC_RATIO=0.7 +DATABASE_BADGER_CLEANUP_INTERVAL=5m +DATABASE_BADGER_LRU=100 + +# STORAGE: [mock, minio] +APP_STORAGE=minio +STORAGE_MINIO_HOST=embed-minio +STORAGE_MINIO_PORT=9000 +STORAGE_MINIO_ACCESS_KEY=12345678 +STORAGE_MINIO_SECRET_KEY=qwertyui +STORAGE_MINIO_TOKEN= +STORAGE_MINIO_SECURE=false +STORAGE_MINIO_RETRY_TIMES=4 +STORAGE_MINIO_RETRY_PAUSE=5s +STORAGE_MINIO_TIMEOUT=30s +STORAGE_MINIO_LOCATION=eu-central-1 +STORAGE_MINIO_PREFIX= diff --git a/build/config-embed.yml b/build/config-embed.yml deleted file mode 100644 index 6f664d0..0000000 --- a/build/config-embed.yml +++ /dev/null @@ -1,87 +0,0 @@ -app: - ballast: 0 - log: info -server: - domain: - host: - port: 8001 - h2c: false - readTimeout: 60s - writeTimeout: 120s - idleTimeout: 120s - shutdownTimeout: 10s -middleware: - cors: - allowOrigin: "*" - limiter: - requestsPerSecond: 10000 - burst: 10 -controller: - maxNumberOfFiles: 100 - maxFileSize: 5242880 # 5 MB -service: - numberOfWorkersCalc: 8 - numberOfWorkersComp: 8 - accuracy: 0.625 -cache: - use: mem - redis: - host: embed-redis - port: 6379 - retry: - times: 4 - pause: 5s - timeout: 30s - timeToLive: 15m - cleanupInterval: 15m -compressor: - use: mock - imaginary: - host: embed-imaginary - port: 9001 - retry: - times: 4 - pause: 5s - timeout: 30s - shortpixel: - url: https://api.shortpixel.com/v2/post-reducer.php - url2: https://api.shortpixel.com/v2/reducer.php - apiKey: abcdefghijklmnopqrst - retry: - times: 2 - pause: 10s - timeout: 30s - wait: 30 - uploadTimeout: 60s - downloadTimeout: 60s - repeatIn: 10s - restartIn: 15m -database: - use: badger - mongo: - host: embed-mongo - port: 27017 - retry: - times: 4 - pause: 5s - timeout: 30s - lru: 100 - badger: - gcRatio: 0.7 - cleanupInterval: 5m - lru: 100 -storage: - use: minio - minio: - host: embed-minio - port: 9000 - accessKey: 12345678 - secretKey: qwertyui - token: - secure: false - retry: - times: 4 - pause: 5s - timeout: 30s - location: eu-central-1 - prefix: /s3 diff --git a/build/config-prod.env b/build/config-prod.env new file mode 100644 index 0000000..63500d0 --- /dev/null +++ b/build/config-prod.env @@ -0,0 +1,93 @@ +# CONFIG +CONFIG_RELOAD=true +CONFIG_RELOAD_INTERVAL=1s + +# APP +APP_NAME=config-prod +APP_LOG=info +APP_GC_TUNER=custom # [none, custom, go] +APP_MEM_TOTAL=2147483648 +APP_MEM_LIMIT_RATIO=0.7 + +# SERVER +SERVER_DOMAIN= +SERVER_HOST= +SERVER_PORT=8001 +SERVER_H2C=true +SERVER_READ_TIMEOUT=60s +SERVER_WRITE_TIMEOUT=120s +SERVER_IDLE_TIMEOUT=120s +SERVER_SHUTDOWN_TIMEOUT=10s + +# MIDDLEWARE +MIDDLEWARE_CORS_ALLOW_ORIGIN=* +MIDDLEWARE_LIMITER_REQUESTS_PER_SECOND=30000 +MIDDLEWARE_LIMITER_BURST=300 +MIDDLEWARE_DEBUG=false + +# CONTROLLER +CONTROLLER_MAX_NUMBER_OF_FILES=100 +CONTROLLER_MAX_FILE_SIZE=5242880 # 5 MB + +# SERVICE +SERVICE_TEMP_LINKS=true +SERVICE_NUMBER_OF_WORKERS_CALC=8 +SERVICE_NUMBER_OF_WORKERS_COMP=8 +SERVICE_ACCURACY=0.625 + +# CACHE: [mem, redis] +APP_CACHE=redis +CACHE_MEM_CLEANUP_INTERVAL=15m +CACHE_REDIS_HOST=prod-redis +CACHE_REDIS_PORT=6379 +CACHE_REDIS_RETRY_TIMES=4 +CACHE_REDIS_RETRY_PAUSE=5s +CACHE_REDIS_TIMEOUT=30s +CACHE_REDIS_TIME_TO_LIVE=15m +CACHE_REDIS_TX_RETRIES=5 + +# COMPRESSOR: [mock, shortpixel, imaginary] +APP_COMPRESSOR=mock +COMPRESSOR_SHORTPIXEL_URL=https://api.shortpixel.com/v2/post-reducer.php +COMPRESSOR_SHORTPIXEL_URL2=https://api.shortpixel.com/v2/reducer.php +COMPRESSOR_SHORTPIXEL_API_KEY=abcdefghijklmnopqrst +COMPRESSOR_SHORTPIXEL_RETRY_TIMES=2 +COMPRESSOR_SHORTPIXEL_RETRY_PAUSE=10s +COMPRESSOR_SHORTPIXEL_TIMEOUT=30s +COMPRESSOR_SHORTPIXEL_WAIT=30 +COMPRESSOR_SHORTPIXEL_UPLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_DOWNLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_REPEAT_IN=10s +COMPRESSOR_SHORTPIXEL_RESTART_IN=15m +COMPRESSOR_IMAGINARY_HOST=prod-imaginary +COMPRESSOR_IMAGINARY_PORT=9001 +COMPRESSOR_IMAGINARY_RETRY_TIMES=4 +COMPRESSOR_IMAGINARY_RETRY_PAUSE=5s +COMPRESSOR_IMAGINARY_TIMEOUT=30s + +# DATABASE: [mem, mongo, badger] +APP_DATABASE=mongo +DATABASE_MONGO_HOST=prod-mongo +DATABASE_MONGO_PORT=27017 +DATABASE_MONGO_RETRY_TIMES=4 +DATABASE_MONGO_RETRY_PAUSE=5s +DATABASE_MONGO_TIMEOUT=30s +DATABASE_MONGO_LRU=100 +DATABASE_BADGER_IN_MEMORY=false +DATABASE_BADGER_GC_RATIO=0.7 +DATABASE_BADGER_CLEANUP_INTERVAL=5m +DATABASE_BADGER_LRU=100 + +# STORAGE: [mock, minio] +APP_STORAGE=minio +STORAGE_MINIO_HOST=prod-minio +STORAGE_MINIO_PORT=9000 +STORAGE_MINIO_ACCESS_KEY=12345678 +STORAGE_MINIO_SECRET_KEY=qwertyui +STORAGE_MINIO_TOKEN= +STORAGE_MINIO_SECURE=false +STORAGE_MINIO_RETRY_TIMES=4 +STORAGE_MINIO_RETRY_PAUSE=5s +STORAGE_MINIO_TIMEOUT=30s +STORAGE_MINIO_LOCATION=eu-central-1 +STORAGE_MINIO_PREFIX= diff --git a/build/config-prod.yml b/build/config-prod.yml deleted file mode 100644 index a18210b..0000000 --- a/build/config-prod.yml +++ /dev/null @@ -1,87 +0,0 @@ -app: - ballast: 0 - log: info -server: - domain: - host: - port: 8001 - h2c: true - readTimeout: 60s - writeTimeout: 120s - idleTimeout: 120s - shutdownTimeout: 10s -middleware: - cors: - allowOrigin: "*" - limiter: - requestsPerSecond: 10000 - burst: 10 -controller: - maxNumberOfFiles: 100 - maxFileSize: 5242880 # 5 MB -service: - numberOfWorkersCalc: 8 - numberOfWorkersComp: 8 - accuracy: 0.625 -cache: - use: redis - redis: - host: prod-redis - port: 6379 - retry: - times: 4 - pause: 5s - timeout: 30s - timeToLive: 15m - cleanupInterval: 15m -compressor: - use: imaginary - imaginary: - host: prod-imaginary - port: 9001 - retry: - times: 4 - pause: 5s - timeout: 30s - shortpixel: - url: https://api.shortpixel.com/v2/post-reducer.php - url2: https://api.shortpixel.com/v2/reducer.php - apiKey: abcdefghijklmnopqrst - retry: - times: 2 - pause: 10s - timeout: 30s - wait: 30 - uploadTimeout: 60s - downloadTimeout: 60s - repeatIn: 10s - restartIn: 15m -database: - use: mongo - mongo: - host: prod-mongo - port: 27017 - retry: - times: 4 - pause: 5s - timeout: 30s - lru: 100 - badger: - gcRatio: 0.7 - cleanupInterval: 5m - lru: 100 -storage: - use: minio - minio: - host: prod-minio - port: 9000 - accessKey: 12345678 - secretKey: qwertyui - token: - secure: false - retry: - times: 4 - pause: 5s - timeout: 30s - location: eu-central-1 - prefix: /s3 diff --git a/build/docker-compose-dev.yml b/build/docker-compose-dev.yml index 208d541..f8a4ba5 100644 --- a/build/docker-compose-dev.yml +++ b/build/docker-compose-dev.yml @@ -2,8 +2,8 @@ services: redis: build: - context: . - dockerfile: Dockerfile-redis + context: ./../ + dockerfile: ./build/Dockerfile-redis container_name: dev-redis ports: - "6379:6379" @@ -12,16 +12,16 @@ services: imaginary: build: - context: . - dockerfile: Dockerfile-imaginary + context: ./../ + dockerfile: ./build/Dockerfile-imaginary container_name: dev-imaginary ports: - "9001:9001" mongo: build: - context: . - dockerfile: Dockerfile-mongo + context: ./../ + dockerfile: ./build/Dockerfile-mongo container_name: dev-mongo ports: - "27017:27017" @@ -30,8 +30,8 @@ services: minio: build: - context: . - dockerfile: Dockerfile-minio + context: ./../ + dockerfile: ./build/Dockerfile-minio container_name: dev-minio ports: - "9000:9000" @@ -39,12 +39,10 @@ services: swagger: build: - context: . - dockerfile: Dockerfile-swagger + context: ./../ + dockerfile: ./build/Dockerfile-swagger container_name: dev-swagger ports: - - "8080:8080" + - "8081:8081" volumes: - "./swagger.yml:/swagger.yml" - environment: - - SWAGGER_JSON=/swagger.yml diff --git a/build/docker-compose-embed.yml b/build/docker-compose-embed.yml index 2656f18..c842e31 100644 --- a/build/docker-compose-embed.yml +++ b/build/docker-compose-embed.yml @@ -5,18 +5,26 @@ services: context: ./../ dockerfile: ./build/Dockerfile-app container_name: embed-app + deploy: + resources: + limits: + memory: ${APP_MEM_TOTAL} + restart_policy: + condition: on-failure + max_attempts: 3 + window: 120s ports: - "8001:8001" volumes: - - "./config-embed.yml:/config.yml" + - "./config-embed.env:/config.env" - "./badger/:/badger/" depends_on: - minio minio: build: - context: . - dockerfile: Dockerfile-minio + context: ./../ + dockerfile: ./build/Dockerfile-minio container_name: embed-minio volumes: - "./minio/data/:/data/" diff --git a/build/docker-compose-prod.yml b/build/docker-compose-prod.yml index b361595..76fe66d 100644 --- a/build/docker-compose-prod.yml +++ b/build/docker-compose-prod.yml @@ -2,8 +2,8 @@ services: caddy: build: - context: . - dockerfile: Dockerfile-caddy + context: ./../ + dockerfile: ./build/Dockerfile-caddy container_name: prod-caddy ports: - "80:80" @@ -21,8 +21,16 @@ services: context: ./../ dockerfile: ./build/Dockerfile-app container_name: prod-app + deploy: + resources: + limits: + memory: ${APP_MEM_TOTAL} + restart_policy: + condition: on-failure + max_attempts: 3 + window: 120s volumes: - - "./config-prod.yml:/config.yml" + - "./config-prod.env:/config.env" depends_on: - redis - imaginary @@ -31,22 +39,22 @@ services: redis: build: - context: . - dockerfile: Dockerfile-redis + context: ./../ + dockerfile: ./build/Dockerfile-redis container_name: prod-redis volumes: - "./redis.conf:/usr/local/etc/redis/redis.conf" imaginary: build: - context: . - dockerfile: Dockerfile-imaginary + context: ./../ + dockerfile: ./build/Dockerfile-imaginary container_name: prod-imaginary mongo: build: - context: . - dockerfile: Dockerfile-mongo + context: ./../ + dockerfile: ./build/Dockerfile-mongo container_name: prod-mongo volumes: - "./mongo.js:/docker-entrypoint-initdb.d/mongo.js" @@ -62,8 +70,8 @@ services: minio: build: - context: . - dockerfile: Dockerfile-minio + context: ./../ + dockerfile: ./build/Dockerfile-minio container_name: prod-minio volumes: - "./minio/data/:/data/" diff --git a/build/swagger.yml b/build/swagger.yml index beef5fa..f276288 100644 --- a/build/swagger.yml +++ b/build/swagger.yml @@ -22,19 +22,11 @@ paths: responses: '201': $ref: '#/components/responses/AlbumResponse' - '400': - $ref: '#/components/responses/BadRequest' - '413': - $ref: '#/components/responses/PayloadTooLarge' - '415': - $ref: '#/components/responses/UnsupportedMediaType' - '429': - $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' '503': $ref: '#/components/responses/ServiceUnavailable' - /api/albums/{id}/ready/: + /api/albums/{album}/status/: get: description: > Second request in a sequence (optional). It informs about @@ -45,16 +37,12 @@ paths: - $ref: '#/components/parameters/albumParam' responses: '200': - $ref: '#/components/responses/ReadyResponse' - '404': - $ref: '#/components/responses/NotFound' - '429': - $ref: '#/components/responses/TooManyRequests' + $ref: '#/components/responses/StatusResponse' '500': $ref: '#/components/responses/InternalServerError' '503': $ref: '#/components/responses/ServiceUnavailable' - /api/albums/{id}/pair/: + /api/albums/{album}/pair/: get: description: > Third request in a sequence. Response consists of 2 image @@ -64,15 +52,26 @@ paths: responses: '200': $ref: '#/components/responses/PairResponse' - '404': - $ref: '#/components/responses/NotFound' - '429': - $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' '503': $ref: '#/components/responses/ServiceUnavailable' - /api/albums/{id}/vote/: + /api/images/{token}/: + get: + description: > + If the backend is configured to hide an image ID, it will return + a temporary link to a file which can be requested under this + endpoint. + parameters: + - $ref: '#/components/parameters/tokenParam' + responses: + '200': + $ref: '#/components/responses/PairResponse' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + /api/albums/{album}/vote/: patch: description: > Fourth request in a sequence. Request specifies a value transfer @@ -86,17 +85,11 @@ paths: responses: '200': $ref: '#/components/responses/VoteResponse' - '404': - $ref: '#/components/responses/NotFound' - '415': - $ref: '#/components/responses/UnsupportedMediaType' - '429': - $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' '503': $ref: '#/components/responses/ServiceUnavailable' - /api/albums/{id}/top/: + /api/albums/{album}/top/: get: description: > Fifth request in a sequence. Returns a list of all images in an @@ -107,10 +100,17 @@ paths: responses: '200': $ref: '#/components/responses/TopResponse' - '404': - $ref: '#/components/responses/NotFound' - '429': - $ref: '#/components/responses/TooManyRequests' + '500': + $ref: '#/components/responses/InternalServerError' + '503': + $ref: '#/components/responses/ServiceUnavailable' + /api/health/: + get: + description: > + Returns the health of the service and its dependencies. + responses: + '200': + description: OK '500': $ref: '#/components/responses/InternalServerError' '503': @@ -138,7 +138,7 @@ components: properties: id: $ref: '#/components/schemas/Id' - ReadyResponse: + StatusResponse: type: object properties: album: @@ -208,12 +208,20 @@ components: error: type: object properties: + code: + type: integer msg: type: string parameters: albumParam: in: path - name: id + name: album + required: true + schema: + $ref: '#/components/schemas/Id' + tokenParam: + in: path + name: token required: true schema: $ref: '#/components/schemas/Id' @@ -235,12 +243,12 @@ components: application/json: schema: $ref: '#/components/schemas/AlbumResponse' - ReadyResponse: + StatusResponse: description: OK content: application/json: schema: - $ref: '#/components/schemas/ReadyResponse' + $ref: '#/components/schemas/StatusResponse' PairResponse: description: OK content: @@ -255,36 +263,6 @@ components: application/json: schema: $ref: '#/components/schemas/TopResponse' - BadRequest: - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - NotFound: - description: Not Found - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - PayloadTooLarge: - description: Payload Too Large - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - UnsupportedMediaType: - description: Unsupported Media Type - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - TooManyRequests: - description: Too Many Requests - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' InternalServerError: description: Internal Server Error content: diff --git a/cmd/healthcheck/main.go b/cmd/healthcheck/main.go new file mode 100644 index 0000000..3ba825a --- /dev/null +++ b/cmd/healthcheck/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "flag" + "os" + + "github.com/zitryss/aye-and-nay/internal/client" + "github.com/zitryss/aye-and-nay/internal/config" +) + +func main() { + path := "" + flag.StringVar(&path, "config", "./config.env", "filepath to a config file") + flag.Parse() + conf, err := config.New(path) + if err != nil { + os.Exit(1) + } + api := "http://localhost:" + conf.Server.Port + timeout := conf.Server.WriteTimeout + c, err := client.New(api, timeout) + if err != nil { + os.Exit(1) + } + err = c.Health() + if err != nil { + os.Exit(1) + } +} diff --git a/cmd/loadtest/loadtest.go b/cmd/loadtest/loadtest.go new file mode 100644 index 0000000..83dbc3b --- /dev/null +++ b/cmd/loadtest/loadtest.go @@ -0,0 +1,152 @@ +package main + +import ( + "net/http" + "strings" + + "github.com/zitryss/aye-and-nay/internal/client" + "github.com/zitryss/aye-and-nay/pkg/errors" + "github.com/zitryss/aye-and-nay/pkg/log" +) + +type loadtest struct { + client *client.Client + err error +} + +func (l *loadtest) albumApi() string { + if l.err != nil { + return "" + } + album, err := l.client.Album() + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return "" + } + return album +} + +func (l *loadtest) statusApi(album string) { + if l.err != nil { + return + } + err := l.client.Status(album) + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return + } +} + +func (l *loadtest) pairApi(album string) client.Pair { + if l.err != nil { + return client.Pair{} + } + pairs, err := l.client.Pair(album) + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return client.Pair{} + } + return pairs +} + +func (l *loadtest) voteApi(album string, token1 string, token2 string) { + if l.err != nil { + return + } + err := l.client.Vote(album, token1, token2) + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return + } +} + +func (l *loadtest) topApi(album string) []string { + if l.err != nil { + return nil + } + src, err := l.client.Top(album) + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return nil + } + return src +} + +func (l *loadtest) healthApi() { + if l.err != nil { + return + } + err := l.client.Health() + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return + } +} + +func (l *loadtest) albumHtml() { + l.html("/index.html") +} + +func (l *loadtest) pairHtml() { + l.html("/pair.html") +} + +func (l *loadtest) topHtml() { + l.html("/top.html") +} + +func (l *loadtest) html(page string) { + if l.err != nil { + return + } + if !html { + return + } + err := l.client.Do(http.MethodGet, apiAddress+page, http.NoBody) + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return + } +} + +func (l *loadtest) pairMinio(src1 string, src2 string) { + l.minio(src1) + l.minio(src2) +} + +func (l *loadtest) topMinio(src []string) { + for _, s := range src { + l.minio(s) + } +} + +func (l *loadtest) minio(src string) { + if l.err != nil { + return + } + if !minio { + return + } + address := "" + if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") { + address = src + } else { + address = apiAddress + src + } + if !strings.HasPrefix(src, "/api/images/") { + return // FIXME + } + err := l.client.Do(http.MethodGet, address, http.NoBody) + if err != nil { + l.err = errors.Wrap(err) + log.Error(l.err) + return + } +} diff --git a/cmd/loadtest/main.go b/cmd/loadtest/main.go index f283403..cdf769b 100644 --- a/cmd/loadtest/main.go +++ b/cmd/loadtest/main.go @@ -1,285 +1,115 @@ -// The purpose of this tool is to provide a realistic load on the -// system. It creates "n" albums (default 2), each contains 20 images. -// In total, it sends n x 94 requests. package main import ( - "bytes" - "crypto/tls" - "encoding/json" + "context" "flag" - "fmt" - "io" - "mime/multipart" - "net/http" "os" - "strings" + "os/signal" + "sync" + "syscall" "time" - "github.com/cheggaaa/pb/v3" - - "github.com/zitryss/aye-and-nay/pkg/debug" + "github.com/zitryss/aye-and-nay/internal/client" + "github.com/zitryss/aye-and-nay/pkg/log" ) var ( - n int - apiAddress string - minioAddress string - connections int - testdata string - verbose bool - b []byte - sep string + duration time.Duration + connections int + timeout time.Duration + testdata string + apiAddress string + html bool + minio bool + verbose bool ) func main() { - flag.IntVar(&n, "n", 2, "#albums") - flag.StringVar(&apiAddress, "api-address", "https://localhost", "") - flag.StringVar(&minioAddress, "minio-address", "https://localhost", "") + flag.DurationVar(&duration, "duration", 10*time.Second, "in seconds") flag.IntVar(&connections, "connections", 2, "") + flag.DurationVar(&timeout, "timeout", 5*time.Second, "in seconds") flag.StringVar(&testdata, "testdata", "./testdata", "") + flag.StringVar(&apiAddress, "api-address", "https://localhost", "") + flag.BoolVar(&html, "html", true, "") + flag.BoolVar(&minio, "minio", true, "") flag.BoolVar(&verbose, "verbose", true, "") flag.Parse() - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - bar := pb.StartNew(n * 94) - if !verbose { - bar.SetWriter(io.Discard) + if verbose { + log.SetOutput(os.Stderr) + log.SetLevel(log.INFO) } - readFiles() + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + wg := sync.WaitGroup{} + wg.Add(2) - sem := make(chan struct{}, connections) - for i := 0; i < n; i++ { - sem <- struct{}{} - go func() { - defer func() { <-sem }() - albumHtml() - album := albumApi() - bar.Increment() - readyApi(album) - bar.Increment() - for j := 0; j < 4; j++ { - pairHtml() - for k := 0; k < 11; k++ { - src1, token1, src2, token2 := pairApi(album) - bar.Increment() - pairMinio(src1, src2) - voteApi(album, token1, token2) - bar.Increment() - } - topHtml() - src := topApi(album) - bar.Increment() - topMinio(src) + c, err := client.New(apiAddress, timeout*time.Second, client.WithFiles(testdata), client.WithTimes(5)) + if err != nil { + log.Critical(err) + os.Exit(1) + } + + go func() { + defer wg.Done() + passed1sAgo := 0 + for { + select { + case <-ctx.Done(): + return + case <-time.After(1 * time.Second): + passed, failed := c.Stats() + log.Infof( + "%d rps, %d passed (%.2f%%), %d failed (%.2f%%)\n", + passed-passed1sAgo, + passed, float64(passed)/float64(passed+failed)*100, + failed, float64(failed)/float64(passed+failed)*100, + ) + passed1sAgo = passed } - }() - } - for i := 0; i < connections; i++ { - sem <- struct{}{} - } - - bar.Finish() - fmt.Println(time.Since(bar.StartTime())) - fmt.Println(float64(n*94)/time.Since(bar.StartTime()).Seconds(), "rps") -} - -func readFiles() { - body := bytes.Buffer{} - multi := multipart.NewWriter(&body) - for i := 0; i < 4; i++ { - for _, filename := range []string{"alan.jpg", "john.bmp", "dennis.png", "tim.gif", "big.jpg"} { - part, err := multi.CreateFormFile("images", filename) - debug.Check(err) - b, err := os.ReadFile(testdata + "/" + filename) - debug.Check(err) - _, err = part.Write(b) - debug.Check(err) - } - } - err := multi.WriteField("duration", "1h") - debug.Check(err) - err = multi.Close() - debug.Check(err) - - b = body.Bytes() - sep = multi.FormDataContentType() -} - -func albumHtml() { - html("/index.html") -} - -func albumApi() string { - body := bytes.NewReader(b) - req, err := http.NewRequest("POST", apiAddress+"/api/albums/", body) - debug.Check(err) - req.Header.Set("Content-Type", sep) - - resp, err := http.DefaultClient.Do(req) - debug.Check(err) - debug.Assert(resp.StatusCode == 201) - - type result struct { - Album struct { - Id string - } - } - - res := result{} - err = json.NewDecoder(resp.Body).Decode(&res) - debug.Check(err) - err = resp.Body.Close() - debug.Check(err) - - return res.Album.Id -} - -func readyApi(album string) { - req, err := http.NewRequest("GET", apiAddress+"/api/albums/"+album+"/ready/", nil) - debug.Check(err) - - resp, err := http.DefaultClient.Do(req) - debug.Check(err) - debug.Assert(resp.StatusCode == 200) - - type result struct { - Album struct { - Progress float64 } - } - - res := result{} - err = json.NewDecoder(resp.Body).Decode(&res) - debug.Check(err) - err = resp.Body.Close() - debug.Check(err) -} - -func pairHtml() { - html("/pair.html") -} - -func pairApi(album string) (string, string, string, string) { - req, err := http.NewRequest("GET", apiAddress+"/api/albums/"+album+"/pair/", nil) - debug.Check(err) - - resp, err := http.DefaultClient.Do(req) - debug.Check(err) - debug.Assert(resp.StatusCode == 200) - - type result struct { - Album struct { - Img1 struct { - Token string - Src string - } - Img2 struct { - Token string - Src string + }() + + go func() { + defer wg.Done() + start := time.Now() + sem := make(chan struct{}, connections) + for time.Since(start) < duration { + select { + case sem <- struct{}{}: + case <-ctx.Done(): + return } + go func() { + defer func() { <-sem }() + l := loadtest{client: c} + l.albumHtml() + album := l.albumApi() + l.statusApi(album) + for j := 0; j < 5; j++ { + l.pairHtml() + for k := 0; k < 10; k++ { + pairs := l.pairApi(album) + l.pairMinio(pairs.One.Src, pairs.Two.Src) + l.voteApi(album, pairs.One.Token, pairs.Two.Token) + } + l.topHtml() + src := l.topApi(album) + l.topMinio(src) + } + l.healthApi() + }() } - } - - res := result{} - err = json.NewDecoder(resp.Body).Decode(&res) - debug.Check(err) - err = resp.Body.Close() - debug.Check(err) - - return res.Album.Img1.Src, res.Album.Img1.Token, res.Album.Img2.Src, res.Album.Img2.Token -} - -func pairMinio(src1 string, src2 string) { - minio(src1) - minio(src2) -} - -func voteApi(album string, token1 string, token2 string) { - body := strings.NewReader("{\"album\":{\"imgFrom\":{\"token\":\"" + token1 + "\"},\"imgTo\":{\"token\":\"" + token2 + "\"}}}") - req, err := http.NewRequest("PATCH", apiAddress+"/api/albums/"+album+"/vote/", body) - debug.Check(err) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - resp, err := http.DefaultClient.Do(req) - debug.Check(err) - debug.Assert(resp.StatusCode == 200) - _, err = io.Copy(io.Discard, resp.Body) - debug.Check(err) - err = resp.Body.Close() - debug.Check(err) -} - -func topHtml() { - html("/top.html") -} - -func topApi(album string) []string { - req, err := http.NewRequest("GET", apiAddress+"/api/albums/"+album+"/top/", nil) - debug.Check(err) - - resp, err := http.DefaultClient.Do(req) - debug.Check(err) - debug.Assert(resp.StatusCode == 200) - - type image struct { - Src string - Rating float64 - } - type result struct { - Album struct { - Images []image + for i := 0; i < connections; i++ { + sem <- struct{}{} } - } + stop() + }() - res := result{} - err = json.NewDecoder(resp.Body).Decode(&res) - debug.Check(err) - err = resp.Body.Close() - debug.Check(err) + wg.Wait() - src := []string(nil) - for _, image := range res.Album.Images { - src = append(src, image.Src) - } - return src -} - -func topMinio(src []string) { - for _, s := range src { - minio(s) - } -} - -func html(page string) { - if apiAddress != minioAddress { - return - } - req, err := http.NewRequest("GET", apiAddress+page, nil) - if err != nil { - return - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return - } - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() -} - -func minio(src string) { - if minioAddress == "" { - return - } - req, err := http.NewRequest("GET", minioAddress+src, nil) - if err != nil { - return - } - resp, err := http.DefaultClient.Do(req) - if err != nil { - return + _, failed := c.Stats() + if failed > 0 { + os.Exit(1) } - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() } diff --git a/codecov.yml b/codecov.yml index a8fb609..698c3eb 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,5 +8,8 @@ ignore: - "main.go" - "tools.go" - "cmd/loadtest/main.go" + - "cmd/healthcheck/main.go" - "delivery/http/requests_easyjson.go" - "delivery/http/responses_easyjson.go" + - "internal/**/*" + - "pkg/log/level_string.go" diff --git a/config.env b/config.env new file mode 100644 index 0000000..eed8ec7 --- /dev/null +++ b/config.env @@ -0,0 +1,93 @@ +# CONFIG +CONFIG_RELOAD=true +CONFIG_RELOAD_INTERVAL=1s + +# APP +APP_NAME=config +APP_LOG=info +APP_GC_TUNER=custom # [none, custom, go] +APP_MEM_TOTAL=2147483648 # 2 GB +APP_MEM_LIMIT_RATIO=0.7 + +# SERVER +SERVER_DOMAIN= +SERVER_HOST=localhost +SERVER_PORT=8001 +SERVER_H2C=false +SERVER_READ_TIMEOUT=60s +SERVER_WRITE_TIMEOUT=120s +SERVER_IDLE_TIMEOUT=120s +SERVER_SHUTDOWN_TIMEOUT=10s + +# MIDDLEWARE +MIDDLEWARE_CORS_ALLOW_ORIGIN=* +MIDDLEWARE_LIMITER_REQUESTS_PER_SECOND=30000 +MIDDLEWARE_LIMITER_BURST=300 +MIDDLEWARE_DEBUG=true + +# CONTROLLER +CONTROLLER_MAX_NUMBER_OF_FILES=100 +CONTROLLER_MAX_FILE_SIZE=5242880 # 5 MB + +# SERVICE +SERVICE_TEMP_LINKS=true +SERVICE_NUMBER_OF_WORKERS_CALC=8 +SERVICE_NUMBER_OF_WORKERS_COMP=8 +SERVICE_ACCURACY=0.625 + +# CACHE: [mem, redis] +APP_CACHE=mem +CACHE_MEM_CLEANUP_INTERVAL=15m +CACHE_REDIS_HOST=localhost +CACHE_REDIS_PORT=6379 +CACHE_REDIS_RETRY_TIMES=4 +CACHE_REDIS_RETRY_PAUSE=5s +CACHE_REDIS_TIMEOUT=30s +CACHE_REDIS_TIME_TO_LIVE=15m +CACHE_REDIS_TX_RETRIES=5 + +# COMPRESSOR: [mock, shortpixel, imaginary] +APP_COMPRESSOR=mock +COMPRESSOR_SHORTPIXEL_URL=https://api.shortpixel.com/v2/post-reducer.php +COMPRESSOR_SHORTPIXEL_URL2=https://api.shortpixel.com/v2/reducer.php +COMPRESSOR_SHORTPIXEL_API_KEY=abcdefghijklmnopqrst +COMPRESSOR_SHORTPIXEL_RETRY_TIMES=2 +COMPRESSOR_SHORTPIXEL_RETRY_PAUSE=10s +COMPRESSOR_SHORTPIXEL_TIMEOUT=30s +COMPRESSOR_SHORTPIXEL_WAIT=30 +COMPRESSOR_SHORTPIXEL_UPLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_DOWNLOAD_TIMEOUT=60s +COMPRESSOR_SHORTPIXEL_REPEAT_IN=10s +COMPRESSOR_SHORTPIXEL_RESTART_IN=15m +COMPRESSOR_IMAGINARY_HOST=localhost +COMPRESSOR_IMAGINARY_PORT=9001 +COMPRESSOR_IMAGINARY_RETRY_TIMES=4 +COMPRESSOR_IMAGINARY_RETRY_PAUSE=5s +COMPRESSOR_IMAGINARY_TIMEOUT=30s + +# DATABASE: [mem, mongo, badger] +APP_DATABASE=mem +DATABASE_MONGO_HOST=localhost +DATABASE_MONGO_PORT=27017 +DATABASE_MONGO_RETRY_TIMES=4 +DATABASE_MONGO_RETRY_PAUSE=5s +DATABASE_MONGO_TIMEOUT=30s +DATABASE_MONGO_LRU=100 +DATABASE_BADGER_IN_MEMORY=false +DATABASE_BADGER_GC_RATIO=0.7 +DATABASE_BADGER_CLEANUP_INTERVAL=5m +DATABASE_BADGER_LRU=100 + +# STORAGE: [mock, minio] +APP_STORAGE=mock +STORAGE_MINIO_HOST=localhost +STORAGE_MINIO_PORT=9000 +STORAGE_MINIO_ACCESS_KEY=12345678 +STORAGE_MINIO_SECRET_KEY=qwertyui +STORAGE_MINIO_TOKEN= +STORAGE_MINIO_SECURE=false +STORAGE_MINIO_RETRY_TIMES=4 +STORAGE_MINIO_RETRY_PAUSE=5s +STORAGE_MINIO_TIMEOUT=30s +STORAGE_MINIO_LOCATION=eu-central-1 +STORAGE_MINIO_PREFIX= diff --git a/config.yml b/config.yml deleted file mode 100644 index ea4bdd5..0000000 --- a/config.yml +++ /dev/null @@ -1,87 +0,0 @@ -app: - ballast: 0 - log: info -server: - domain: - host: localhost - port: 8001 - h2c: false - readTimeout: 60s - writeTimeout: 120s - idleTimeout: 120s - shutdownTimeout: 10s -middleware: - cors: - allowOrigin: "*" - limiter: - requestsPerSecond: 10000 - burst: 10 -controller: - maxNumberOfFiles: 100 - maxFileSize: 5242880 # 5 MB -service: - numberOfWorkersCalc: 8 - numberOfWorkersComp: 8 - accuracy: 0.625 -cache: - use: mem - redis: - host: localhost - port: 6379 - retry: - times: 4 - pause: 5s - timeout: 30s - timeToLive: 15m - cleanupInterval: 15m -compressor: - use: mock - imaginary: - host: localhost - port: 9001 - retry: - times: 4 - pause: 5s - timeout: 30s - shortpixel: - url: https://api.shortpixel.com/v2/post-reducer.php - url2: https://api.shortpixel.com/v2/reducer.php - apiKey: abcdefghijklmnopqrst - retry: - times: 2 - pause: 10s - timeout: 30s - wait: 30 - uploadTimeout: 60s - downloadTimeout: 60s - repeatIn: 10s - restartIn: 15m -database: - use: mem - mongo: - host: localhost - port: 27017 - retry: - times: 4 - pause: 5s - timeout: 30s - lru: 100 - badger: - gcRatio: 0.7 - cleanupInterval: 5m - lru: 100 -storage: - use: mock - minio: - host: localhost - port: 9000 - accessKey: 12345678 - secretKey: qwertyui - token: - secure: false - retry: - times: 4 - pause: 5s - timeout: 30s - location: eu-central-1 - prefix: diff --git a/delivery/http/config.go b/delivery/http/config.go index afa5ba9..fad0acd 100644 --- a/delivery/http/config.go +++ b/delivery/http/config.go @@ -2,52 +2,56 @@ package http import ( "time" - - "github.com/spf13/viper" ) -func newServerConfig() serverConfig { - return serverConfig{ - domain: viper.GetString("server.domain"), - host: viper.GetString("server.host"), - port: viper.GetString("server.port"), - h2c: viper.GetBool("server.h2c"), - readTimeout: viper.GetDuration("server.readTimeout"), - writeTimeout: viper.GetDuration("server.writeTimeout"), - idleTimeout: viper.GetDuration("server.idleTimeout"), - shutdownTimeout: viper.GetDuration("server.shutdownTimeout"), - } -} +const ( + kb = 1 << (10 * 1) +) -type serverConfig struct { - domain string - host string - port string - h2c bool - readTimeout time.Duration - writeTimeout time.Duration - idleTimeout time.Duration - shutdownTimeout time.Duration +type ServerConfig struct { + Domain string `mapstructure:"SERVER_DOMAIN"` + Host string `mapstructure:"SERVER_HOST"` + Port string `mapstructure:"SERVER_PORT" validate:"required"` + H2C bool `mapstructure:"SERVER_H2C"` + ReadTimeout time.Duration `mapstructure:"SERVER_READ_TIMEOUT" validate:"required"` + WriteTimeout time.Duration `mapstructure:"SERVER_WRITE_TIMEOUT" validate:"required"` + IdleTimeout time.Duration `mapstructure:"SERVER_IDLE_TIMEOUT" validate:"required"` + ShutdownTimeout time.Duration `mapstructure:"SERVER_SHUTDOWN_TIMEOUT" validate:"required"` + Controller ControllerConfig `mapstructure:",squash"` } -func newMiddlewareConfig() middlewareConfig { - return middlewareConfig{ - corsAllowOrigin: viper.GetString("middleware.cors.allowOrigin"), - } +type MiddlewareConfig struct { + CorsAllowOrigin string `mapstructure:"MIDDLEWARE_CORS_ALLOW_ORIGIN" validate:"required"` + WriteTimeout time.Duration `mapstructure:"SERVER_WRITE_TIMEOUT" validate:"required"` + MaxFileSize int64 `mapstructure:"CONTROLLER_MAX_FILE_SIZE" validate:"required"` + Debug bool `mapstructure:"MIDDLEWARE_DEBUG"` } -type middlewareConfig struct { - corsAllowOrigin string +type ControllerConfig struct { + MaxNumberOfFiles int `mapstructure:"CONTROLLER_MAX_NUMBER_OF_FILES" validate:"required"` + MaxFileSize int64 `mapstructure:"CONTROLLER_MAX_FILE_SIZE" validate:"required"` } -func newContrConfig() contrConfig { - return contrConfig{ - maxNumberOfFiles: viper.GetInt("controller.maxNumberOfFiles"), - maxFileSize: viper.GetInt64("controller.maxFileSize"), +var ( + DefaultServerConfig = ServerConfig{ + Domain: "", + Host: "localhost", + Port: "8001", + H2C: false, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 10 * time.Second, + ShutdownTimeout: 1 * time.Second, + Controller: DefaultControllerConfig, } -} - -type contrConfig struct { - maxNumberOfFiles int - maxFileSize int64 -} + DefaultMiddlewareConfig = MiddlewareConfig{ + CorsAllowOrigin: "", + WriteTimeout: 10 * time.Second, + MaxFileSize: 512 * kb, + Debug: false, + } + DefaultControllerConfig = ControllerConfig{ + MaxNumberOfFiles: 3, + MaxFileSize: 512 * kb, + } +) diff --git a/delivery/http/controller.go b/delivery/http/controller.go index 7ae7a7c..8519338 100644 --- a/delivery/http/controller.go +++ b/delivery/http/controller.go @@ -4,7 +4,9 @@ import ( "context" "encoding/json" "io" + "mime/multipart" "net/http" + "os" "strings" "time" @@ -17,14 +19,14 @@ import ( ) func newController( + conf ControllerConfig, serv domain.Servicer, ) controller { - conf := newContrConfig() return controller{conf, serv} } type controller struct { - conf contrConfig + conf ControllerConfig serv domain.Servicer } @@ -35,7 +37,7 @@ func (c *controller) handleAlbum() httprouter.Handle { if !strings.HasPrefix(ct, "multipart/form-data") { return nil, albumRequest{}, errors.Wrap(domain.ErrWrongContentType) } - maxBodySize := int64(c.conf.maxNumberOfFiles) * c.conf.maxFileSize + maxBodySize := int64(c.conf.MaxNumberOfFiles) * c.conf.MaxFileSize if r.ContentLength > maxBodySize { return nil, albumRequest{}, errors.Wrap(domain.ErrBodyTooLarge) } @@ -47,18 +49,18 @@ func (c *controller) handleAlbum() httprouter.Handle { if len(fhs) < 2 { return nil, albumRequest{}, errors.Wrap(domain.ErrNotEnoughImages) } - if len(fhs) > c.conf.maxNumberOfFiles { + if len(fhs) > c.conf.MaxNumberOfFiles { return nil, albumRequest{}, errors.Wrap(domain.ErrTooManyImages) } req := albumRequest{ff: make([]model.File, 0, len(fhs)), multi: r.MultipartForm} defer func() { for _, f := range req.ff { - _ = f.Reader.(io.Closer).Close() + _ = f.Close() } _ = req.multi.RemoveAll() }() for _, fh := range fhs { - if fh.Size > c.conf.maxFileSize { + if fh.Size > c.conf.MaxFileSize { return nil, albumRequest{}, errors.Wrap(domain.ErrImageTooLarge) } f, err := fh.Open() @@ -81,7 +83,25 @@ func (c *controller) handleAlbum() httprouter.Handle { _ = f.Close() return nil, albumRequest{}, errors.Wrap(domain.ErrNotImage) } - req.ff = append(req.ff, model.File{Reader: f, Size: fh.Size}) + F := model.File{} + switch v := f.(type) { + case *os.File: + closeFn := func() error { + _ = v.Close() + _ = os.Remove(v.Name()) + return nil + } + F = model.NewFile(v, closeFn, fh.Size) + case multipart.File: + closeFn := func() error { + _ = v.Close() + return nil + } + F = model.NewFile(v, closeFn, fh.Size) + default: + return nil, albumRequest{}, errors.Wrap(domain.ErrUnknown) + } + req.ff = append(req.ff, F) } vals := r.MultipartForm.Value["duration"] if len(vals) == 0 { @@ -97,7 +117,7 @@ func (c *controller) handleAlbum() httprouter.Handle { process := func(ctx context.Context, req albumRequest) (albumResponse, error) { defer func() { for _, f := range req.ff { - _ = f.Reader.(io.Closer).Close() + _ = f.Close() } _ = req.multi.RemoveAll() }() @@ -120,45 +140,45 @@ func (c *controller) handleAlbum() httprouter.Handle { return nil } return handleHttpRouterError( - func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error { + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { ctx, req, err := input(r, ps) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } resp, err := process(ctx, req) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } err = output(ctx, w, resp) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } - return nil + return ctx, nil }, ) } -func (c *controller) handleReady() httprouter.Handle { - input := func(r *http.Request, ps httprouter.Params) (context.Context, readyRequest, error) { +func (c *controller) handleStatus() httprouter.Handle { + input := func(r *http.Request, ps httprouter.Params) (context.Context, statusRequest, error) { ctx := r.Context() - req := readyRequest{} + req := statusRequest{} req.album.id = ps.ByName("album") return ctx, req, nil } - process := func(ctx context.Context, req readyRequest) (readyResponse, error) { + process := func(ctx context.Context, req statusRequest) (statusResponse, error) { album, err := base64.ToUint64(req.album.id) if err != nil { - return readyResponse{}, errors.Wrap(err) + return statusResponse{}, errors.Wrap(domain.ErrInvalidId) } p, err := c.serv.Progress(ctx, album) if err != nil { - return readyResponse{}, errors.Wrap(err) + return statusResponse{}, errors.Wrap(err) } - resp := readyResponse{} - resp.Album.Progress = p + resp := statusResponse{} + resp.Album.Compression.Progress = p return resp, nil } - output := func(ctx context.Context, w http.ResponseWriter, resp readyResponse) error { + output := func(ctx context.Context, w http.ResponseWriter, resp statusResponse) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") err := json.NewEncoder(w).Encode(resp) if err != nil { @@ -167,20 +187,20 @@ func (c *controller) handleReady() httprouter.Handle { return nil } return handleHttpRouterError( - func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error { + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { ctx, req, err := input(r, ps) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } resp, err := process(ctx, req) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } err = output(ctx, w, resp) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } - return nil + return ctx, nil }, ) } @@ -195,7 +215,7 @@ func (c *controller) handlePair() httprouter.Handle { process := func(ctx context.Context, req pairRequest) (pairResponse, error) { album, err := base64.ToUint64(req.album.id) if err != nil { - return pairResponse{}, errors.Wrap(err) + return pairResponse{}, errors.Wrap(domain.ErrInvalidId) } img1, img2, err := c.serv.Pair(ctx, album) if err != nil { @@ -219,20 +239,66 @@ func (c *controller) handlePair() httprouter.Handle { return nil } return handleHttpRouterError( - func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error { + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { ctx, req, err := input(r, ps) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } resp, err := process(ctx, req) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } err = output(ctx, w, resp) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } - return nil + return ctx, nil + }, + ) +} + +func (c *controller) handleImage() httprouter.Handle { + input := func(r *http.Request, ps httprouter.Params) (context.Context, imageRequest, error) { + ctx := r.Context() + req := imageRequest{} + req.image.token = ps.ByName("token") + return ctx, req, nil + } + process := func(ctx context.Context, req imageRequest) (imageResponse, error) { + token, err := base64.ToUint64(req.image.token) + if err != nil { + return imageResponse{}, errors.Wrap(domain.ErrInvalidId) + } + f, err := c.serv.Image(ctx, token) + if err != nil { + return imageResponse{}, errors.Wrap(err) + } + resp := imageResponse{f} + return resp, nil + } + output := func(ctx context.Context, w http.ResponseWriter, resp imageResponse) error { + defer resp.f.Close() + _, err := io.Copy(w, resp.f.Reader) + if err != nil { + return errors.Wrap(err) + } + return nil + } + return handleHttpRouterError( + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { + ctx, req, err := input(r, ps) + if err != nil { + return ctx, errors.Wrap(err) + } + resp, err := process(ctx, req) + if err != nil { + return ctx, errors.Wrap(err) + } + err = output(ctx, w, resp) + if err != nil { + return ctx, errors.Wrap(err) + } + return ctx, nil }, ) } @@ -255,15 +321,15 @@ func (c *controller) handleVote() httprouter.Handle { process := func(ctx context.Context, req voteRequest) (voteResponse, error) { album, err := base64.ToUint64(req.Album.id) if err != nil { - return voteResponse{}, errors.Wrap(err) + return voteResponse{}, errors.Wrap(domain.ErrInvalidId) } imgFromToken, err := base64.ToUint64(req.Album.ImgFrom.Token) if err != nil { - return voteResponse{}, errors.Wrap(err) + return voteResponse{}, errors.Wrap(domain.ErrInvalidId) } imgToToken, err := base64.ToUint64(req.Album.ImgTo.Token) if err != nil { - return voteResponse{}, errors.Wrap(err) + return voteResponse{}, errors.Wrap(domain.ErrInvalidId) } err = c.serv.Vote(ctx, album, imgFromToken, imgToToken) if err != nil { @@ -276,20 +342,20 @@ func (c *controller) handleVote() httprouter.Handle { return nil } return handleHttpRouterError( - func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error { + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { ctx, req, err := input(r, ps) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } resp, err := process(ctx, req) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } err = output(ctx, w, resp) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } - return nil + return ctx, nil }, ) } @@ -304,7 +370,7 @@ func (c *controller) handleTop() httprouter.Handle { process := func(ctx context.Context, req topRequest) (topResponse, error) { album, err := base64.ToUint64(req.album.id) if err != nil { - return topResponse{}, errors.Wrap(err) + return topResponse{}, errors.Wrap(domain.ErrInvalidId) } imgs, err := c.serv.Top(ctx, album) if err != nil { @@ -327,20 +393,33 @@ func (c *controller) handleTop() httprouter.Handle { return nil } return handleHttpRouterError( - func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error { + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { ctx, req, err := input(r, ps) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } resp, err := process(ctx, req) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } err = output(ctx, w, resp) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) + } + return ctx, nil + }, + ) +} + +func (c *controller) handleHealth() httprouter.Handle { + return handleHttpRouterError( + func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error) { + ctx := r.Context() + _, err := c.serv.Health(ctx) + if err != nil { + return ctx, errors.Wrap(err) } - return nil + return ctx, nil }, ) } diff --git a/delivery/http/controller_test.go b/delivery/http/controller_test.go index 59bd77a..e0b6500 100644 --- a/delivery/http/controller_test.go +++ b/delivery/http/controller_test.go @@ -1,10 +1,9 @@ -//go:build unit - package http import ( "bytes" "context" + "io" "mime/multipart" "net/http" "net/http/httptest" @@ -13,748 +12,302 @@ import ( "testing" "github.com/julienschmidt/httprouter" + "github.com/stretchr/testify/assert" "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/service" - _ "github.com/zitryss/aye-and-nay/internal/config" . "github.com/zitryss/aye-and-nay/internal/testing" ) -func TestControllerHandleAlbum(t *testing.T) { +func TestControllerHandle(t *testing.T) { + if !*unit { + t.Skip() + } type give struct { - err error - filenames []string - durationOn bool - duration string + handle func() httprouter.Handle + method string + target string + reqBody io.Reader + headers map[string]string + params []httprouter.Param } type want struct { - code int - typ string - body string + code int + typ string + respBody string } + contr := controller{} + payload := content{} tests := []struct { give want }{ { give: give{ - err: nil, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", - }, - want: want{ - code: 201, - typ: "application/json; charset=utf-8", - body: `{"album":{"id":"rRsAAAAAAAA"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"big.jpg", "big.jpg", "big.jpg"}, - durationOn: true, - duration: "1h", - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":1,"msg":"body too large"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"alan.jpg"}, - durationOn: true, - duration: "1h", - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":3,"msg":"not enough images"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png", "alan.jpg"}, - durationOn: true, - duration: "1h", - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":4,"msg":"too many images"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"alan.jpg", "john.bmp", "big.jpg"}, - durationOn: true, - duration: "1h", - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":5,"msg":"image too large"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"alan.jpg", "john.bmp", "audio.ogg"}, - durationOn: true, - duration: "1h", - }, - want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":6,"msg":"unsupported media type"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: false, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":7,"msg":"duration not set"}}` + "\n", - }, - }, - { - give: give{ - err: nil, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "", - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":8,"msg":"duration invalid"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTooManyRequests, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg", "john.bmp", "dennis.png"}, true, "1h"), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 429, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":0,"msg":"too many requests"}}` + "\n", + code: http.StatusCreated, + typ: "application/json; charset=utf-8", + respBody: `{"album":{"id":"rRsAAAAAAAA"}}` + "\n", }, }, { give: give{ - err: domain.ErrBodyTooLarge, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"big.jpg", "big.jpg", "big.jpg"}, true, "1h"), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":1,"msg":"body too large"}}` + "\n", + code: http.StatusRequestEntityTooLarge, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":2,"msg":"body too large"}}` + "\n", }, }, { give: give{ - err: domain.ErrWrongContentType, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg"}, true, "1h"), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":2,"msg":"unsupported media type"}}` + "\n", + code: http.StatusBadRequest, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":4,"msg":"not enough images"}}` + "\n", }, }, { give: give{ - err: domain.ErrNotEnoughImages, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg", "john.bmp", "dennis.png", "alan.jpg"}, true, "1h"), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":3,"msg":"not enough images"}}` + "\n", + code: http.StatusRequestEntityTooLarge, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":5,"msg":"too many images"}}` + "\n", }, }, { give: give{ - err: domain.ErrTooManyImages, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "11h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg", "john.bmp", "big.jpg"}, true, "1h"), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":4,"msg":"too many images"}}` + "\n", + code: http.StatusRequestEntityTooLarge, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":6,"msg":"image too large"}}` + "\n", }, }, { give: give{ - err: domain.ErrImageTooLarge, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg", "john.bmp", "audio.ogg"}, true, "1h"), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":5,"msg":"image too large"}}` + "\n", + code: http.StatusUnsupportedMediaType, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":7,"msg":"unsupported media type"}}` + "\n", }, }, { give: give{ - err: domain.ErrNotImage, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg", "john.bmp", "dennis.png"}, false, ""), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":6,"msg":"unsupported media type"}}` + "\n", + code: http.StatusBadRequest, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":8,"msg":"duration not set"}}` + "\n", }, }, { give: give{ - err: domain.ErrDurationNotSet, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleAlbum, + method: http.MethodPost, + target: "/api/albums/", + reqBody: payload.body(t, []string{"alan.jpg", "john.bmp", "dennis.png"}, true, ""), + headers: map[string]string{"Content-Type": payload.boundary}, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":7,"msg":"duration not set"}}` + "\n", + code: http.StatusBadRequest, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":9,"msg":"duration invalid"}}` + "\n", }, }, { give: give{ - err: domain.ErrDurationInvalid, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleStatus, + method: http.MethodGet, + target: "/api/albums/rRsAAAAAAAA/status", + params: httprouter.Params{httprouter.Param{Key: "album", Value: "rRsAAAAAAAA"}}, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":8,"msg":"duration invalid"}}` + "\n", + code: http.StatusOK, + typ: "application/json; charset=utf-8", + respBody: `{"album":{"compression":{"progress":1}}}` + "\n", }, }, { give: give{ - err: domain.ErrAlbumNotFound, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handlePair, + method: http.MethodGet, + target: "/api/albums/nkUAAAAAAAA/", + params: httprouter.Params{httprouter.Param{Key: "album", Value: "nkUAAAAAAAA"}}, }, want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":9,"msg":"album not found"}}` + "\n", + code: http.StatusOK, + typ: "application/json; charset=utf-8", + respBody: `{"album":{"img1":{"token":"f8cAAAAAAAA","src":"/aye-and-nay/albums/nkUAAAAAAAA/images/21EAAAAAAAA"},"img2":{"token":"iakAAAAAAAA","src":"/aye-and-nay/albums/nkUAAAAAAAA/images/K2IAAAAAAAA"}}}` + "\n", }, }, { give: give{ - err: domain.ErrTokenNotFound, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleImage, + method: http.MethodGet, + target: "/api/images/8v7AAAAAAAA/", + params: httprouter.Params{httprouter.Param{Key: "token", Value: "8v7AAAAAAAA"}}, }, want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":11,"msg":"token not found"}}` + "\n", + code: http.StatusOK, + typ: "image/png", + respBody: png(), }, }, { give: give{ - err: domain.ErrThirdPartyUnavailable, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleVote, + method: http.MethodPatch, + target: "/api/albums/fIIAAAAAAAA/", + reqBody: strings.NewReader(`{"album":{"imgFrom":{"token":"fYIAAAAAAAA"},"imgTo":{"token":"foIAAAAAAAA"}}}`), + headers: map[string]string{"Content-Type": "application/json; charset=utf-8"}, + params: httprouter.Params{httprouter.Param{Key: "album", Value: "fIIAAAAAAAA"}}, }, want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":16,"msg":"internal server error"}}` + "\n", + code: http.StatusOK, + typ: "", + respBody: ``, }, }, { give: give{ - err: context.Canceled, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleTop, + method: http.MethodGet, + target: "/api/albums/byYAAAAAAAA/top/", + params: httprouter.Params{httprouter.Param{Key: "album", Value: "byYAAAAAAAA"}}, }, want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-1,"msg":"internal server error"}}` + "\n", + code: http.StatusOK, + typ: "application/json; charset=utf-8", + respBody: `{"album":{"images":[{"src":"/aye-and-nay/albums/byYAAAAAAAA/images/yFwAAAAAAAA","rating":0.5},{"src":"/aye-and-nay/albums/byYAAAAAAAA/images/jVgAAAAAAAA","rating":0.5}]}}` + "\n", }, }, { give: give{ - err: context.DeadlineExceeded, - filenames: []string{"alan.jpg", "john.bmp", "dennis.png"}, - durationOn: true, - duration: "1h", + handle: contr.handleHealth, + method: http.MethodGet, + target: "/api/health/", }, want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-2,"msg":"internal server error"}}` + "\n", + code: http.StatusOK, + typ: "", + respBody: ``, }, }, } for _, tt := range tests { t.Run("", func(t *testing.T) { - serv := service.NewMock(tt.give.err) - contr := newController(serv) - fn := contr.handleAlbum() + err := error(nil) + serv := service.NewMock(err) + contr = newController(DefaultControllerConfig, serv) + fn := tt.give.handle() w := httptest.NewRecorder() - body := bytes.Buffer{} - multi := multipart.NewWriter(&body) - for _, filename := range tt.give.filenames { - part, err := multi.CreateFormFile("images", filename) - if err != nil { - t.Error(err) - } - b, err := os.ReadFile("../../testdata/" + filename) - if err != nil { - t.Error(err) - } - _, err = part.Write(b) - if err != nil { - t.Error(err) - } - } - if tt.give.durationOn { - err := multi.WriteField("duration", tt.give.duration) - if err != nil { - t.Error(err) - } + r := httptest.NewRequest(tt.give.method, tt.give.target, tt.give.reqBody) + for k, v := range tt.give.headers { + r.Header.Set(k, v) } - err := multi.Close() - if err != nil { - t.Error(err) - } - r := httptest.NewRequest(http.MethodPost, "/api/albums/", &body) - r.Header.Set("Content-Type", multi.FormDataContentType()) - fn(w, r, nil) - CheckStatusCode(t, w, tt.want.code) - CheckContentType(t, w, tt.want.typ) - CheckBody(t, w, tt.want.body) + fn(w, r, tt.give.params) + AssertStatusCode(t, w, tt.want.code) + AssertHeader(t, w, "Content-Type", tt.want.typ) + AssertBody(t, w, tt.want.respBody) }) } } -func TestControllerHandleReady(t *testing.T) { - type give struct { - err error - } - type want struct { - code int - typ string - body string - } - tests := []struct { - give - want - }{ - { - give: give{ - err: nil, - }, - want: want{ - code: 200, - typ: "application/json; charset=utf-8", - body: `{"album":{"progress":1}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTooManyRequests, - }, - want: want{ - code: 429, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":0,"msg":"too many requests"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrBodyTooLarge, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":1,"msg":"body too large"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrWrongContentType, - }, - want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":2,"msg":"unsupported media type"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrNotEnoughImages, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":3,"msg":"not enough images"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTooManyImages, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":4,"msg":"too many images"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrImageTooLarge, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":5,"msg":"image too large"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrNotImage, - }, - want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":6,"msg":"unsupported media type"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrDurationNotSet, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":7,"msg":"duration not set"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrDurationInvalid, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":8,"msg":"duration invalid"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrAlbumNotFound, - }, - want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":9,"msg":"album not found"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTokenNotFound, - }, - want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":11,"msg":"token not found"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrThirdPartyUnavailable, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":16,"msg":"internal server error"}}` + "\n", - }, - }, - { - give: give{ - err: context.Canceled, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-1,"msg":"internal server error"}}` + "\n", - }, - }, - { - give: give{ - err: context.DeadlineExceeded, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-2,"msg":"internal server error"}}` + "\n", - }, - }, - } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - serv := service.NewMock(tt.give.err) - contr := newController(serv) - fn := contr.handleReady() - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/api/albums/rRsAAAAAAAA/ready", nil) - ps := httprouter.Params{httprouter.Param{Key: "album", Value: "rRsAAAAAAAA"}} - fn(w, r, ps) - CheckStatusCode(t, w, tt.want.code) - CheckContentType(t, w, tt.want.typ) - CheckBody(t, w, tt.want.body) - }) - } +type content struct { + boundary string } -func TestControllerHandlePair(t *testing.T) { - type give struct { - err error - } - type want struct { - code int - typ string - body string - } - tests := []struct { - give - want - }{ - { - give: give{ - err: nil, - }, - want: want{ - code: 200, - typ: "application/json; charset=utf-8", - body: `{"album":{"img1":{"token":"f8cAAAAAAAA","src":"/aye-and-nay/albums/nkUAAAAAAAA/images/21EAAAAAAAA"},"img2":{"token":"iakAAAAAAAA","src":"/aye-and-nay/albums/nkUAAAAAAAA/images/K2IAAAAAAAA"}}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTooManyRequests, - }, - want: want{ - code: 429, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":0,"msg":"too many requests"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrBodyTooLarge, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":1,"msg":"body too large"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrWrongContentType, - }, - want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":2,"msg":"unsupported media type"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrNotEnoughImages, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":3,"msg":"not enough images"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTooManyImages, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":4,"msg":"too many images"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrImageTooLarge, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":5,"msg":"image too large"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrNotImage, - }, - want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":6,"msg":"unsupported media type"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrDurationNotSet, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":7,"msg":"duration not set"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrDurationInvalid, - }, - want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":8,"msg":"duration invalid"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrAlbumNotFound, - }, - want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":9,"msg":"album not found"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTokenNotFound, - }, - want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":11,"msg":"token not found"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrThirdPartyUnavailable, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":16,"msg":"internal server error"}}` + "\n", - }, - }, - { - give: give{ - err: context.Canceled, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-1,"msg":"internal server error"}}` + "\n", - }, - }, - { - give: give{ - err: context.DeadlineExceeded, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-2,"msg":"internal server error"}}` + "\n", - }, - }, +func (c *content) body(t *testing.T, filenames []string, durationOn bool, duration string) io.Reader { + t.Helper() + body := bytes.Buffer{} + multi := multipart.NewWriter(&body) + for _, filename := range filenames { + part, err := multi.CreateFormFile("images", filename) + assert.NoError(t, err) + b, err := os.ReadFile("../../testdata/" + filename) + assert.NoError(t, err) + _, err = part.Write(b) + assert.NoError(t, err) } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - serv := service.NewMock(tt.give.err) - contr := newController(serv) - fn := contr.handlePair() - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/api/albums/nkUAAAAAAAA/", nil) - ps := httprouter.Params{httprouter.Param{Key: "album", Value: "nkUAAAAAAAA"}} - fn(w, r, ps) - CheckStatusCode(t, w, tt.want.code) - CheckContentType(t, w, tt.want.typ) - CheckBody(t, w, tt.want.body) - }) + if durationOn { + err := multi.WriteField("duration", duration) + assert.NoError(t, err) } + err := multi.Close() + assert.NoError(t, err) + c.boundary = multi.FormDataContentType() + return &body +} + +func png() string { + body, _ := io.ReadAll(Png()) + return string(body) } -func TestControllerHandleVote(t *testing.T) { +func TestControllerError(t *testing.T) { + if !*unit { + t.Skip() + } type give struct { err error } type want struct { - code int - typ string - body string + code int + typ string + respBody string } tests := []struct { give want }{ - { - give: give{ - err: nil, - }, - want: want{ - code: 200, - typ: "", - body: ``, - }, - }, { give: give{ err: domain.ErrTooManyRequests, }, want: want{ - code: 429, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":0,"msg":"too many requests"}}` + "\n", + code: http.StatusTooManyRequests, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":1,"msg":"too many requests"}}` + "\n", }, }, { @@ -762,9 +315,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrBodyTooLarge, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":1,"msg":"body too large"}}` + "\n", + code: http.StatusRequestEntityTooLarge, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":2,"msg":"body too large"}}` + "\n", }, }, { @@ -772,9 +325,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrWrongContentType, }, want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":2,"msg":"unsupported media type"}}` + "\n", + code: http.StatusUnsupportedMediaType, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":3,"msg":"unsupported media type"}}` + "\n", }, }, { @@ -782,9 +335,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrNotEnoughImages, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":3,"msg":"not enough images"}}` + "\n", + code: http.StatusBadRequest, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":4,"msg":"not enough images"}}` + "\n", }, }, { @@ -792,9 +345,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrTooManyImages, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":4,"msg":"too many images"}}` + "\n", + code: http.StatusRequestEntityTooLarge, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":5,"msg":"too many images"}}` + "\n", }, }, { @@ -802,9 +355,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrImageTooLarge, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":5,"msg":"image too large"}}` + "\n", + code: http.StatusRequestEntityTooLarge, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":6,"msg":"image too large"}}` + "\n", }, }, { @@ -812,9 +365,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrNotImage, }, want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":6,"msg":"unsupported media type"}}` + "\n", + code: http.StatusUnsupportedMediaType, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":7,"msg":"unsupported media type"}}` + "\n", }, }, { @@ -822,9 +375,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrDurationNotSet, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":7,"msg":"duration not set"}}` + "\n", + code: http.StatusBadRequest, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":8,"msg":"duration not set"}}` + "\n", }, }, { @@ -832,9 +385,9 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrDurationInvalid, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":8,"msg":"duration invalid"}}` + "\n", + code: http.StatusBadRequest, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":9,"msg":"duration invalid"}}` + "\n", }, }, { @@ -842,211 +395,129 @@ func TestControllerHandleVote(t *testing.T) { err: domain.ErrAlbumNotFound, }, want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":9,"msg":"album not found"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrTokenNotFound, - }, - want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":11,"msg":"token not found"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrThirdPartyUnavailable, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":16,"msg":"internal server error"}}` + "\n", - }, - }, - { - give: give{ - err: context.Canceled, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-1,"msg":"internal server error"}}` + "\n", + code: http.StatusNotFound, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":10,"msg":"album not found"}}` + "\n", }, }, { give: give{ - err: context.DeadlineExceeded, - }, - want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-2,"msg":"internal server error"}}` + "\n", - }, - }, - } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - serv := service.NewMock(tt.give.err) - contr := newController(serv) - fn := contr.handleVote() - w := httptest.NewRecorder() - json := strings.NewReader(`{"album":{"imgFrom":{"token":"fYIAAAAAAAA"},"imgTo":{"token":"foIAAAAAAAA"}}}`) - r := httptest.NewRequest(http.MethodPatch, "/api/albums/fIIAAAAAAAA/", json) - r.Header.Set("Content-Type", "application/json; charset=utf-8") - ps := httprouter.Params{httprouter.Param{Key: "album", Value: "fIIAAAAAAAA"}} - fn(w, r, ps) - CheckStatusCode(t, w, tt.want.code) - CheckContentType(t, w, tt.want.typ) - CheckBody(t, w, tt.want.body) - }) - } -} - -func TestControllerHandleTop(t *testing.T) { - type give struct { - err error - } - type want struct { - code int - typ string - body string - } - tests := []struct { - give - want - }{ - { - give: give{ - err: nil, + err: domain.ErrPairNotFound, }, want: want{ - code: 200, - typ: "application/json; charset=utf-8", - body: `{"album":{"images":[{"src":"/aye-and-nay/albums/byYAAAAAAAA/images/yFwAAAAAAAA","rating":0.5},{"src":"/aye-and-nay/albums/byYAAAAAAAA/images/jVgAAAAAAAA","rating":0.5}]}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":11,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrTooManyRequests, + err: domain.ErrTokenNotFound, }, want: want{ - code: 429, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":0,"msg":"too many requests"}}` + "\n", + code: http.StatusNotFound, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":12,"msg":"token not found"}}` + "\n", }, }, { give: give{ - err: domain.ErrBodyTooLarge, + err: domain.ErrImageNotFound, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":1,"msg":"body too large"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":13,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrWrongContentType, + err: domain.ErrAlbumAlreadyExists, }, want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":2,"msg":"unsupported media type"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":14,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrNotEnoughImages, + err: domain.ErrTokenAlreadyExists, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":3,"msg":"not enough images"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":15,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrTooManyImages, + err: domain.ErrUnsupportedMediaType, }, want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":4,"msg":"too many images"}}` + "\n", + code: http.StatusUnsupportedMediaType, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":16,"msg":"unsupported media type"}}` + "\n", }, }, { give: give{ - err: domain.ErrImageTooLarge, - }, - want: want{ - code: 413, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":5,"msg":"image too large"}}` + "\n", - }, - }, - { - give: give{ - err: domain.ErrNotImage, + err: domain.ErrThirdPartyUnavailable, }, want: want{ - code: 415, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":6,"msg":"unsupported media type"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":17,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrDurationNotSet, + err: domain.ErrBadHealthCompressor, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":7,"msg":"duration not set"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":18,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrDurationInvalid, + err: domain.ErrBadHealthStorage, }, want: want{ - code: 400, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":8,"msg":"duration invalid"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":19,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrAlbumNotFound, + err: domain.ErrBadHealthDatabase, }, want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":9,"msg":"album not found"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":20,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrTokenNotFound, + err: domain.ErrBadHealthCache, }, want: want{ - code: 404, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":11,"msg":"token not found"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":21,"msg":"internal server error"}}` + "\n", }, }, { give: give{ - err: domain.ErrThirdPartyUnavailable, + err: domain.ErrUnknown, }, want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":16,"msg":"internal server error"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":22,"msg":"internal server error"}}` + "\n", }, }, { @@ -1054,9 +525,9 @@ func TestControllerHandleTop(t *testing.T) { err: context.Canceled, }, want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-1,"msg":"internal server error"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":-1,"msg":"internal server error"}}` + "\n", }, }, { @@ -1064,24 +535,23 @@ func TestControllerHandleTop(t *testing.T) { err: context.DeadlineExceeded, }, want: want{ - code: 500, - typ: "application/json; charset=utf-8", - body: `{"error":{"code":-2,"msg":"internal server error"}}` + "\n", + code: http.StatusInternalServerError, + typ: "application/json; charset=utf-8", + respBody: `{"error":{"code":-2,"msg":"internal server error"}}` + "\n", }, }, } for _, tt := range tests { t.Run("", func(t *testing.T) { serv := service.NewMock(tt.give.err) - contr := newController(serv) - fn := contr.handleTop() + contr := newController(DefaultControllerConfig, serv) + fn := contr.handleHealth() w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/api/albums/byYAAAAAAAA/top/", nil) - ps := httprouter.Params{httprouter.Param{Key: "album", Value: "byYAAAAAAAA"}} - fn(w, r, ps) - CheckStatusCode(t, w, tt.want.code) - CheckContentType(t, w, tt.want.typ) - CheckBody(t, w, tt.want.body) + r := httptest.NewRequest(http.MethodGet, "/api/health/", http.NoBody) + fn(w, r, nil) + AssertStatusCode(t, w, tt.want.code) + AssertHeader(t, w, "Content-Type", tt.want.typ) + AssertBody(t, w, tt.want.respBody) }) } } diff --git a/delivery/http/error.go b/delivery/http/error.go index 95d26af..6cbdef1 100644 --- a/delivery/http/error.go +++ b/delivery/http/error.go @@ -12,28 +12,28 @@ import ( "github.com/zitryss/aye-and-nay/pkg/errors" ) -func handleHttpRouterError(fn func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) error) httprouter.Handle { +func handleHttpRouterError(fn func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) (context.Context, error)) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - err := fn(w, r, ps) + ctx, err := fn(w, r, ps) if err == nil { return } - handleError(w, err) + handleError(ctx, w, err) } } -func handleHttpError(fn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc { +func handleHttpError(fn func(w http.ResponseWriter, r *http.Request) (context.Context, error)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - err := fn(w, r) + ctx, err := fn(w, r) if err == nil { return } - handleError(w, err) + handleError(ctx, w, err) } } -func handleError(w http.ResponseWriter, err error) { - service.HandleInnerError(err) +func handleError(ctx context.Context, w http.ResponseWriter, err error) { + service.HandleInnerError(ctx, err) handleOuterError(w, err) } @@ -41,13 +41,11 @@ func handleOuterError(w http.ResponseWriter, err error) { resp := errorResponse{} defer func() { w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(resp.Error.statusCode) _ = json.NewEncoder(w).Encode(resp) }() cause := errors.Cause(err) - e := domain.Error(nil) - if errors.As(cause, &e) { + if e := domain.Error(nil); errors.As(cause, &e) { out := e.Outer() resp.Error.statusCode = out.StatusCode resp.Error.AppCode = out.AppCode diff --git a/delivery/http/middleware.go b/delivery/http/middleware.go index 22d312b..3171ed5 100644 --- a/delivery/http/middleware.go +++ b/delivery/http/middleware.go @@ -1,37 +1,66 @@ package http import ( + "context" "hash/fnv" "io" "net/http" + "strings" + "time" "github.com/rs/cors" "github.com/zitryss/aye-and-nay/domain/domain" + "github.com/zitryss/aye-and-nay/internal/log" + "github.com/zitryss/aye-and-nay/internal/requestid" "github.com/zitryss/aye-and-nay/pkg/errors" ) -func NewMiddleware(lim domain.Limiter) *Middleware { - conf := newMiddlewareConfig() +func NewMiddleware( + conf MiddlewareConfig, + lim domain.Limiter, +) *Middleware { return &Middleware{conf, lim} } type Middleware struct { - conf middlewareConfig + conf MiddlewareConfig lim domain.Limiter } func (m *Middleware) Chain(h http.Handler) http.Handler { c := cors.New(cors.Options{ - AllowedOrigins: []string{m.conf.corsAllowOrigin}, + AllowedOrigins: []string{m.conf.CorsAllowOrigin}, AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPatch}, + MaxAge: 86400, // Firefox caps the value at 86400 (24 hours) while all Chromium-based browsers cap it at 7200 (2 hours) }) - return c.Handler(m.recover(m.limit(h))) + if m.conf.Debug { + h = m.debug(h) + } + handler := + m.recover( + m.limit( + c.Handler( + http.TimeoutHandler( + http.MaxBytesHandler( + m.requestId( + m.headers(h), + ), + m.conf.MaxFileSize, + ), + m.conf.WriteTimeout, + "", + ), + ), + ), + ) + return handler } func (m *Middleware) recover(h http.Handler) http.Handler { return handleHttpError( - func(w http.ResponseWriter, r *http.Request) (e error) { + func(w http.ResponseWriter, r *http.Request) (ctx context.Context, e error) { + ctx = r.Context() defer func() { v := recover() if v == nil { @@ -52,23 +81,73 @@ func (m *Middleware) recover(h http.Handler) http.Handler { func (m *Middleware) limit(h http.Handler) http.Handler { return handleHttpError( - func(w http.ResponseWriter, r *http.Request) error { + func(w http.ResponseWriter, r *http.Request) (context.Context, error) { ctx := r.Context() - ip := r.RemoteAddr hash := fnv.New64a() - _, err := io.WriteString(hash, ip) + _, err := io.WriteString(hash, ip(r)) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } allowed, err := m.lim.Allow(ctx, hash.Sum64()) if err != nil { - return errors.Wrap(err) + return ctx, errors.Wrap(err) } if !allowed { - return errors.Wrap(domain.ErrTooManyRequests) + return ctx, errors.Wrap(domain.ErrTooManyRequests) } h.ServeHTTP(w, r) - return nil + return ctx, nil + }, + ) +} + +func ip(r *http.Request) string { + xff := r.Header.Get("X-Forwarded-For") + if xff != "" { + before, _, _ := strings.Cut(xff, ", ") + return before + } + before, _, _ := strings.Cut(r.RemoteAddr, ":") + return before +} + +func (m *Middleware) requestId(h http.Handler) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + ctx := requestid.Set(r.Context()) + r = r.WithContext(ctx) + h.ServeHTTP(w, r) + }, + ) +} + +func (m *Middleware) headers(h http.Handler) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Content-Type-Options", "nosniff") + h.ServeHTTP(w, r) + }, + ) +} + +func (m *Middleware) debug(h http.Handler) http.Handler { + return http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log.Debug(ctx, + "incoming request", + "time", time.Now(), + "r.RemoteAddr", r.RemoteAddr, + "r.Host", r.Host, + "r.Proto", r.Proto, + "r.TLS", r.TLS != nil, + "r.Method", r.Method, + "r.RequestURI", r.RequestURI, + "r.URL.Path", r.URL.Path, + "r.URL.RawQuery", r.URL.RawQuery, + "req.Header", r.Header, + ) + h.ServeHTTP(w, r) }, ) } diff --git a/delivery/http/middleware_test.go b/delivery/http/middleware_test.go index 6b8e005..b7f07ae 100644 --- a/delivery/http/middleware_test.go +++ b/delivery/http/middleware_test.go @@ -1,5 +1,3 @@ -//go:build unit - package http import ( @@ -9,7 +7,9 @@ import ( "net/http/httptest" "testing" - _ "github.com/zitryss/aye-and-nay/internal/config" + "github.com/stretchr/testify/assert" + + "github.com/zitryss/aye-and-nay/internal/requestid" . "github.com/zitryss/aye-and-nay/internal/testing" ) @@ -26,69 +26,198 @@ func (l limiterMockNeg) Allow(_ context.Context, _ uint64) (bool, error) { } func TestMiddlewareRecover(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() t.Run("Positive", func(t *testing.T) { + t.Parallel() fn := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(418) _, _ = io.WriteString(w, "I'm a teapot") } lim := limiterMockPos{} - middle := NewMiddleware(lim) + middle := NewMiddleware(DefaultMiddlewareConfig, lim) handler := middle.recover(http.HandlerFunc(fn)) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) handler.ServeHTTP(w, r) - CheckStatusCode(t, w, 418) - CheckContentType(t, w, "text/plain; charset=utf-8") - CheckBody(t, w, `I'm a teapot`) + AssertStatusCode(t, w, 418) + AssertHeader(t, w, "Content-Type", "text/plain; charset=utf-8") + AssertBody(t, w, `I'm a teapot`) }) t.Run("Negative", func(t *testing.T) { + t.Parallel() fn := func(w http.ResponseWriter, r *http.Request) { panic("don't") } lim := limiterMockPos{} - middle := NewMiddleware(lim) + middle := NewMiddleware(DefaultMiddlewareConfig, lim) handler := middle.recover(http.HandlerFunc(fn)) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) handler.ServeHTTP(w, r) - CheckStatusCode(t, w, 500) - CheckContentType(t, w, "application/json; charset=utf-8") - CheckBody(t, w, `{"error":{"code":17,"msg":"internal server error"}}`+"\n") + AssertStatusCode(t, w, 500) + AssertHeader(t, w, "Content-Type", "application/json; charset=utf-8") + AssertBody(t, w, `{"error":{"code":22,"msg":"internal server error"}}`+"\n") }) } func TestMiddlewareLimit(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() t.Run("Positive", func(t *testing.T) { + t.Parallel() fn := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(418) _, _ = io.WriteString(w, "I'm a teapot") } lim := limiterMockPos{} - middle := NewMiddleware(lim) + middle := NewMiddleware(DefaultMiddlewareConfig, lim) handler := middle.limit(http.HandlerFunc(fn)) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) handler.ServeHTTP(w, r) - CheckStatusCode(t, w, 418) - CheckContentType(t, w, "text/plain; charset=utf-8") - CheckBody(t, w, `I'm a teapot`) + AssertStatusCode(t, w, 418) + AssertHeader(t, w, "Content-Type", "text/plain; charset=utf-8") + AssertBody(t, w, `I'm a teapot`) }) t.Run("Negative", func(t *testing.T) { + t.Parallel() fn := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(418) _, _ = io.WriteString(w, "I'm a teapot") } lim := limiterMockNeg{} - middle := NewMiddleware(lim) + middle := NewMiddleware(DefaultMiddlewareConfig, lim) handler := middle.limit(http.HandlerFunc(fn)) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) handler.ServeHTTP(w, r) - CheckStatusCode(t, w, 429) - CheckContentType(t, w, "application/json; charset=utf-8") - CheckBody(t, w, `{"error":{"code":0,"msg":"too many requests"}}`+"\n") + AssertStatusCode(t, w, 429) + AssertHeader(t, w, "Content-Type", "application/json; charset=utf-8") + AssertBody(t, w, `{"error":{"code":1,"msg":"too many requests"}}`+"\n") }) } + +func TestIP(t *testing.T) { + if !*unit { + t.Skip() + } + type args struct { + xff string + remoteAddr string + } + tests := []struct { + args args + want string + }{ + { + args: args{ + xff: "203.0.113.195", + remoteAddr: "192.168.1.2:65530", + }, + want: "203.0.113.195", + }, + { + args: args{ + xff: "203.0.113.195, 70.41.3.18, 150.172.238.178", + remoteAddr: "192.168.1.2:65530", + }, + want: "203.0.113.195", + }, + { + args: args{ + xff: "2001:db8:85a3:8d3:1319:8a2e:370:7348", + remoteAddr: "192.168.1.2:65530", + }, + want: "2001:db8:85a3:8d3:1319:8a2e:370:7348", + }, + { + args: args{ + xff: "", + remoteAddr: "192.168.1.2:65530", + }, + want: "192.168.1.2", + }, + { + args: args{ + xff: "", + remoteAddr: "", + }, + want: "", + }, + } + t.Parallel() + for _, tt := range tests { + tt := tt + r := httptest.NewRequest(http.MethodGet, "/api/health/", http.NoBody) + r.Header.Set("X-Forwarded-For", tt.args.xff) + r.RemoteAddr = tt.args.remoteAddr + t.Run("", func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, ip(r)) + }) + } +} + +func TestMiddlewareRequestId(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() + ids := []uint64{0} + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := requestid.Get(ctx) + assert.NotZero(t, id) + assert.Positive(t, id) + assert.Greater(t, id, ids[len(ids)-1]) + ids = append(ids, id) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(418) + _, _ = io.WriteString(w, "I'm a teapot") + } + lim := limiterMockPos{} + middle := NewMiddleware(DefaultMiddlewareConfig, lim) + handler := middle.requestId(http.HandlerFunc(fn)) + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + handler.ServeHTTP(w, r) + AssertStatusCode(t, w, 418) + AssertHeader(t, w, "Content-Type", "text/plain; charset=utf-8") + AssertBody(t, w, `I'm a teapot`) + w = httptest.NewRecorder() + r = httptest.NewRequest(http.MethodGet, "/", http.NoBody) + handler.ServeHTTP(w, r) + AssertStatusCode(t, w, 418) + AssertHeader(t, w, "Content-Type", "text/plain; charset=utf-8") + AssertBody(t, w, `I'm a teapot`) +} + +func TestMiddlewareHeaders(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() + fn := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.WriteHeader(418) + _, _ = io.WriteString(w, "I'm a teapot") + } + lim := limiterMockPos{} + middle := NewMiddleware(DefaultMiddlewareConfig, lim) + handler := middle.headers(http.HandlerFunc(fn)) + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", http.NoBody) + handler.ServeHTTP(w, r) + AssertStatusCode(t, w, 418) + AssertHeader(t, w, "Content-Type", "text/plain; charset=utf-8") + AssertHeader(t, w, "X-Content-Type-Options", "nosniff") + AssertBody(t, w, `I'm a teapot`) +} diff --git a/delivery/http/requests.go b/delivery/http/requests.go index 5e49342..9a948b4 100644 --- a/delivery/http/requests.go +++ b/delivery/http/requests.go @@ -15,7 +15,7 @@ type albumRequest struct { dur time.Duration } -type readyRequest struct { +type statusRequest struct { album struct { id string } @@ -27,6 +27,12 @@ type pairRequest struct { } } +type imageRequest struct { + image struct { + token string + } +} + //easyjson:json type voteRequest struct { Album struct { diff --git a/delivery/http/responses.go b/delivery/http/responses.go index e02a76d..4a427c9 100644 --- a/delivery/http/responses.go +++ b/delivery/http/responses.go @@ -2,6 +2,10 @@ package http +import ( + "github.com/zitryss/aye-and-nay/domain/model" +) + //easyjson:json type albumResponse struct { Album struct { @@ -10,9 +14,11 @@ type albumResponse struct { } //easyjson:json -type readyResponse struct { +type statusResponse struct { Album struct { - Progress float64 `json:"progress"` + Compression struct { + Progress float64 `json:"progress"` + } `json:"compression"` } `json:"album"` } @@ -30,6 +36,10 @@ type pairResponse struct { } `json:"album"` } +type imageResponse struct { + f model.File +} + type voteResponse struct { } diff --git a/delivery/http/router.go b/delivery/http/router.go index 46dad5e..84794d5 100644 --- a/delivery/http/router.go +++ b/delivery/http/router.go @@ -8,10 +8,19 @@ import ( func newRouter(contr controller) http.Handler { router := httprouter.New() + // router.POST("/api/albums", contr.handleAlbum()) router.POST("/api/albums/", contr.handleAlbum()) - router.GET("/api/albums/:album/ready/", contr.handleReady()) + // router.GET("/api/albums/:album/status", contr.handleStatus()) + router.GET("/api/albums/:album/status/", contr.handleStatus()) + // router.GET("/api/albums/:album/pair", contr.handlePair()) router.GET("/api/albums/:album/pair/", contr.handlePair()) + // router.GET("/api/images/:token", contr.handleImage()) + router.GET("/api/images/:token/", contr.handleImage()) + // router.PATCH("/api/albums/:album/vote", contr.handleVote()) router.PATCH("/api/albums/:album/vote/", contr.handleVote()) + // router.GET("/api/albums/:album/top", contr.handleTop()) router.GET("/api/albums/:album/top/", contr.handleTop()) + // router.GET("/api/health", contr.handleHealth()) + router.GET("/api/health/", contr.handleHealth()) return router } diff --git a/delivery/http/server.go b/delivery/http/server.go index 1675aac..629bbef 100644 --- a/delivery/http/server.go +++ b/delivery/http/server.go @@ -18,12 +18,12 @@ var ( ) func NewServer( + conf ServerConfig, middle func(http.Handler) http.Handler, serv domain.Servicer, serverWait chan<- error, ) (*Server, error) { - conf := newServerConfig() - contr := newController(serv) + contr := newController(conf.Controller, serv) router := newRouter(contr) handler := middle(router) srv, err := newServer(conf, handler) @@ -33,29 +33,29 @@ func NewServer( return &Server{conf, srv, serverWait}, nil } -func newServer(conf serverConfig, handler http.Handler) (*http.Server, error) { +func newServer(conf ServerConfig, handler http.Handler) (*http.Server, error) { srv := &http.Server{ - Addr: conf.host + ":" + conf.port, - Handler: http.TimeoutHandler(handler, conf.writeTimeout, ""), - ReadTimeout: conf.readTimeout, - WriteTimeout: conf.writeTimeout + 1*time.Second, - IdleTimeout: conf.idleTimeout, + Addr: conf.Host + ":" + conf.Port, + Handler: handler, + ReadTimeout: conf.ReadTimeout, + WriteTimeout: conf.WriteTimeout + 1*time.Second, + IdleTimeout: conf.IdleTimeout, } - if conf.domain != "" { - tlsConfig, err := certmagic.TLS([]string{conf.domain}) + if conf.Domain != "" { + tlsConfig, err := certmagic.TLS([]string{conf.Domain}) if err != nil { return nil, errors.Wrap(err) } srv.TLSConfig = tlsConfig } - if conf.h2c { + if conf.H2C { srv.Handler = h2c.NewHandler(srv.Handler, &http2.Server{}) } return srv, nil } type Server struct { - conf serverConfig + conf ServerConfig srv *http.Server serverWait chan<- error } @@ -76,7 +76,7 @@ func (s *Server) Start() error { } func (s *Server) shutdown() { - ctx, cancel := context.WithTimeout(context.Background(), s.conf.shutdownTimeout) + ctx, cancel := context.WithTimeout(context.Background(), s.conf.ShutdownTimeout) defer cancel() err := s.srv.Shutdown(ctx) s.serverWait <- err diff --git a/delivery/http/server_test.go b/delivery/http/server_test.go new file mode 100644 index 0000000..3bec7ad --- /dev/null +++ b/delivery/http/server_test.go @@ -0,0 +1,69 @@ +package http + +import ( + "flag" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitryss/aye-and-nay/domain/service" + "github.com/zitryss/aye-and-nay/infrastructure/cache" + "github.com/zitryss/aye-and-nay/infrastructure/compressor" + "github.com/zitryss/aye-and-nay/infrastructure/database" + "github.com/zitryss/aye-and-nay/infrastructure/storage" + "github.com/zitryss/aye-and-nay/internal/client" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestServer(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() + comp := compressor.NewMock() + stor := storage.NewMock() + data := database.NewMem(database.DefaultMemConfig) + cach := cache.NewMem(cache.DefaultMemConfig) + qCalc := &service.QueueCalc{} + qComp := &service.QueueComp{} + qDel := &service.QueueDel{} + serv := service.New(service.DefaultServiceConfig, comp, stor, data, cach, qCalc, qComp, qDel) + + middle := NewMiddleware(DefaultMiddlewareConfig, cach) + srvWait := make(chan error, 1) + srv, err := NewServer(DefaultServerConfig, middle.Chain, serv, srvWait) + require.NoError(t, err) + + mockserver := httptest.NewServer(srv.srv.Handler) + defer mockserver.Close() + c, err := client.New(mockserver.URL, 5*time.Second, client.WithFiles("../../testdata"), client.WithTimes(1)) + require.NoError(t, err) + + album, err := c.Album() + assert.NoError(t, err) + err = c.Status(album) + assert.NoError(t, err) + p, err := c.Pair(album) + assert.NoError(t, err) + if service.DefaultServiceConfig.TempLinks == true { + err = c.Do(http.MethodGet, mockserver.URL+p.One.Src, http.NoBody) + assert.NoError(t, err) + err = c.Do(http.MethodGet, mockserver.URL+p.Two.Src, http.NoBody) + assert.NoError(t, err) + } + err = c.Vote(album, p.One.Token, p.Two.Token) + assert.NoError(t, err) + _, err = c.Top(album) + assert.NoError(t, err) + err = c.Health() + assert.NoError(t, err) +} diff --git a/domain/domain/error.go b/domain/domain/error.go index 7f56197..4083c80 100644 --- a/domain/domain/error.go +++ b/domain/domain/error.go @@ -6,209 +6,264 @@ import ( ) const ( - ldisabled int = iota - ldebug - linfo - lerror - lcritical + LogDisabled int = iota + LogDebug + LogInfo + LogError + LogCritical ) var ( ErrTooManyRequests = &domainError{ outerError: outerError{ StatusCode: http.StatusTooManyRequests, - AppCode: 0x0, + AppCode: 0x1, UserMsg: "too many requests", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "too many requests", }, } ErrBodyTooLarge = &domainError{ outerError: outerError{ StatusCode: http.StatusRequestEntityTooLarge, - AppCode: 0x1, + AppCode: 0x2, UserMsg: "body too large", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "body too large", }, } ErrWrongContentType = &domainError{ outerError: outerError{ StatusCode: http.StatusUnsupportedMediaType, - AppCode: 0x2, + AppCode: 0x3, UserMsg: "unsupported media type", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "wrong content type", }, } ErrNotEnoughImages = &domainError{ outerError: outerError{ StatusCode: http.StatusBadRequest, - AppCode: 0x3, + AppCode: 0x4, UserMsg: "not enough images", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "not enough images", }, } ErrTooManyImages = &domainError{ outerError: outerError{ StatusCode: http.StatusRequestEntityTooLarge, - AppCode: 0x4, + AppCode: 0x5, UserMsg: "too many images", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "too many images", }, } ErrImageTooLarge = &domainError{ outerError: outerError{ StatusCode: http.StatusRequestEntityTooLarge, - AppCode: 0x5, + AppCode: 0x6, UserMsg: "image too large", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "image too large", }, } ErrNotImage = &domainError{ outerError: outerError{ StatusCode: http.StatusUnsupportedMediaType, - AppCode: 0x6, + AppCode: 0x7, UserMsg: "unsupported media type", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "not an image", }, } ErrDurationNotSet = &domainError{ outerError: outerError{ StatusCode: http.StatusBadRequest, - AppCode: 0x7, + AppCode: 0x8, UserMsg: "duration not set", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "duration not set", }, } ErrDurationInvalid = &domainError{ outerError: outerError{ StatusCode: http.StatusBadRequest, - AppCode: 0x8, + AppCode: 0x9, UserMsg: "duration invalid", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "duration invalid", }, } + ErrInvalidId = &domainError{ + outerError: outerError{ + StatusCode: http.StatusBadRequest, + AppCode: 0x17, + UserMsg: "id invalid", + }, + innerError: innerError{ + Level: LogDebug, + DevMsg: "id invalid", + }, + } ErrAlbumNotFound = &domainError{ outerError: outerError{ StatusCode: http.StatusNotFound, - AppCode: 0x9, + AppCode: 0xA, UserMsg: "album not found", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "album not found", }, } ErrPairNotFound = &domainError{ outerError: outerError{ StatusCode: http.StatusInternalServerError, - AppCode: 0xA, + AppCode: 0xB, UserMsg: "internal server error", }, innerError: innerError{ - Level: lerror, + Level: LogError, DevMsg: "pair not found", }, } ErrTokenNotFound = &domainError{ outerError: outerError{ StatusCode: http.StatusNotFound, - AppCode: 0xB, + AppCode: 0xC, UserMsg: "token not found", }, innerError: innerError{ - Level: ldebug, + Level: LogDebug, DevMsg: "token not found", }, } ErrImageNotFound = &domainError{ outerError: outerError{ StatusCode: http.StatusInternalServerError, - AppCode: 0xC, + AppCode: 0xD, UserMsg: "internal server error", }, innerError: innerError{ - Level: lerror, + Level: LogError, DevMsg: "image not found", }, } ErrAlbumAlreadyExists = &domainError{ outerError: outerError{ StatusCode: http.StatusInternalServerError, - AppCode: 0xD, + AppCode: 0xE, UserMsg: "internal server error", }, innerError: innerError{ - Level: lerror, + Level: LogError, DevMsg: "album already exists", }, } ErrTokenAlreadyExists = &domainError{ outerError: outerError{ StatusCode: http.StatusInternalServerError, - AppCode: 0xE, + AppCode: 0xF, UserMsg: "internal server error", }, innerError: innerError{ - Level: lerror, + Level: LogError, DevMsg: "token already exists", }, } ErrUnsupportedMediaType = &domainError{ outerError: outerError{ StatusCode: http.StatusUnsupportedMediaType, - AppCode: 0xF, + AppCode: 0x10, UserMsg: "unsupported media type", }, innerError: innerError{ - Level: lerror, + Level: LogError, DevMsg: "image rejected by third party", }, } ErrThirdPartyUnavailable = &domainError{ outerError: outerError{ StatusCode: http.StatusInternalServerError, - AppCode: 0x10, + AppCode: 0x11, + UserMsg: "internal server error", + }, + innerError: innerError{ + Level: LogCritical, + DevMsg: "third party is unavailable", + }, + } + ErrBadHealthCompressor = &domainError{ + outerError: outerError{ + StatusCode: http.StatusInternalServerError, + AppCode: 0x12, + UserMsg: "internal server error", + }, + innerError: innerError{ + Level: LogCritical, + DevMsg: "compressor is unavailable", + }, + } + ErrBadHealthStorage = &domainError{ + outerError: outerError{ + StatusCode: http.StatusInternalServerError, + AppCode: 0x13, + UserMsg: "internal server error", + }, + innerError: innerError{ + Level: LogCritical, + DevMsg: "storage is unavailable", + }, + } + ErrBadHealthDatabase = &domainError{ + outerError: outerError{ + StatusCode: http.StatusInternalServerError, + AppCode: 0x14, UserMsg: "internal server error", }, innerError: innerError{ - Level: lcritical, - DevMsg: "third party unavailable", + Level: LogCritical, + DevMsg: "database is unavailable", + }, + } + ErrBadHealthCache = &domainError{ + outerError: outerError{ + StatusCode: http.StatusInternalServerError, + AppCode: 0x15, + UserMsg: "internal server error", + }, + innerError: innerError{ + Level: LogCritical, + DevMsg: "cache is unavailable", }, } ErrUnknown = &domainError{ outerError: outerError{ StatusCode: http.StatusInternalServerError, - AppCode: 0x11, + AppCode: 0x16, UserMsg: "internal server error", }, innerError: innerError{ - Level: lerror, + Level: LogError, DevMsg: "unknown", }, } diff --git a/domain/domain/interface.go b/domain/domain/interface.go index e0151a1..b2a8b96 100644 --- a/domain/domain/interface.go +++ b/domain/domain/interface.go @@ -10,19 +10,23 @@ import ( type Servicer interface { Album(ctx context.Context, ff []model.File, dur time.Duration) (uint64, error) Pair(ctx context.Context, album uint64) (model.Image, model.Image, error) + Image(ctx context.Context, token uint64) (model.File, error) Vote(ctx context.Context, album uint64, tokenFrom uint64, tokenTo uint64) error Top(ctx context.Context, album uint64) ([]model.Image, error) Progress(ctx context.Context, album uint64) (float64, error) + Checker } type Compresser interface { Compress(ctx context.Context, f model.File) (model.File, error) + Checker } type Storager interface { Put(ctx context.Context, album uint64, image uint64, f model.File) (string, error) Get(ctx context.Context, album uint64, image uint64) (model.File, error) Remove(ctx context.Context, album uint64, image uint64) error + Checker } type Databaser interface { @@ -38,6 +42,7 @@ type Databaser interface { GetImagesOrdered(ctx context.Context, album uint64) ([]model.Image, error) DeleteAlbum(ctx context.Context, album uint64) error AlbumsToBeDeleted(ctx context.Context) ([]model.Album, error) + Checker } type Cacher interface { @@ -46,6 +51,7 @@ type Cacher interface { PQueuer Stacker Tokener + Checker } type Limiter interface { @@ -70,6 +76,11 @@ type Stacker interface { } type Tokener interface { - Set(ctx context.Context, album uint64, token uint64, image uint64) error - Get(ctx context.Context, album uint64, token uint64) (uint64, error) + Set(ctx context.Context, token uint64, album uint64, image uint64) error + Get(ctx context.Context, token uint64) (uint64, uint64, error) + Del(ctx context.Context, token uint64) error +} + +type Checker interface { + Health(ctx context.Context) (bool, error) } diff --git a/domain/model/file.go b/domain/model/file.go index 6074086..b21f318 100644 --- a/domain/model/file.go +++ b/domain/model/file.go @@ -4,7 +4,19 @@ import ( "io" ) +func NewFile(reader io.Reader, close func() error, size int64) File { + return File{reader, close, size} +} + type File struct { io.Reader - Size int64 + close func() error + Size int64 +} + +func (f File) Close() error { + if f.close == nil { + return nil + } + return f.close() } diff --git a/domain/service/config.go b/domain/service/config.go index 29bb564..7331dd1 100644 --- a/domain/service/config.go +++ b/domain/service/config.go @@ -1,19 +1,17 @@ package service -import ( - "github.com/spf13/viper" -) - -func newServiceConfig() serviceConfig { - return serviceConfig{ - numberOfWorkersCalc: viper.GetInt("service.numberOfWorkersCalc"), - numberOfWorkersComp: viper.GetInt("service.numberOfWorkersComp"), - accuracy: viper.GetFloat64("service.accuracy"), - } +type ServiceConfig struct { + TempLinks bool `mapstructure:"SERVICE_TEMP_LINKS"` + NumberOfWorkersCalc int `mapstructure:"SERVICE_NUMBER_OF_WORKERS_CALC" validate:"required"` + NumberOfWorkersComp int `mapstructure:"SERVICE_NUMBER_OF_WORKERS_COMP" validate:"required"` + Accuracy float64 `mapstructure:"SERVICE_ACCURACY" validate:"required"` } -type serviceConfig struct { - numberOfWorkersCalc int - numberOfWorkersComp int - accuracy float64 -} +var ( + DefaultServiceConfig = ServiceConfig{ + TempLinks: true, + NumberOfWorkersCalc: 2, + NumberOfWorkersComp: 2, + Accuracy: 0.625, + } +) diff --git a/domain/service/error.go b/domain/service/error.go index 570afc9..8ee6085 100644 --- a/domain/service/error.go +++ b/domain/service/error.go @@ -4,27 +4,26 @@ import ( "context" "github.com/zitryss/aye-and-nay/domain/domain" + "github.com/zitryss/aye-and-nay/internal/log" "github.com/zitryss/aye-and-nay/pkg/errors" - "github.com/zitryss/aye-and-nay/pkg/log" ) func handleError(err error) { - HandleInnerError(err) + HandleInnerError(context.Background(), err) } -func HandleInnerError(err error) { +func HandleInnerError(ctx context.Context, err error) { cause := errors.Cause(err) - e := domain.Error(nil) - if errors.As(cause, &e) { - log.Println(log.Level(e.Inner().Level), err) + if e := domain.Error(nil); errors.As(cause, &e) { + log.Print(ctx, e.Inner().Level, "err", "stacktrace", err) return } switch cause { case context.Canceled: - log.Debug(err) + log.Debug(ctx, "err", "stacktrace", err) case context.DeadlineExceeded: - log.Debug(err) + log.Debug(ctx, "err", "stacktrace", err) default: - log.Errorf("%T %v", err, err) + log.Error(ctx, "err", "stacktrace", err) } } diff --git a/domain/service/mock.go b/domain/service/mock.go index b3dbfa9..47a2199 100644 --- a/domain/service/mock.go +++ b/domain/service/mock.go @@ -2,9 +2,18 @@ package service import ( "context" + "io" "time" + "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" + . "github.com/zitryss/aye-and-nay/internal/testing" + "github.com/zitryss/aye-and-nay/pkg/errors" + "github.com/zitryss/aye-and-nay/pkg/pool" +) + +var ( + _ domain.Servicer = (*Mock)(nil) ) func NewMock(err error) *Mock { @@ -38,6 +47,19 @@ func (m *Mock) Pair(_ context.Context, _ uint64) (model.Image, model.Image, erro return img1, img2, nil } +func (m *Mock) Image(_ context.Context, _ uint64) (model.File, error) { + if m.err != nil { + return model.File{}, m.err + } + f := Png() + buf := pool.GetBufferN(f.Size) + n, err := io.Copy(buf, f.Reader) + if err != nil { + return model.File{}, errors.Wrap(err) + } + return model.File{Reader: buf, Size: n}, nil +} + func (m *Mock) Vote(_ context.Context, _ uint64, _ uint64, _ uint64) error { if m.err != nil { return m.err @@ -54,3 +76,10 @@ func (m *Mock) Top(_ context.Context, _ uint64) ([]model.Image, error) { imgs := []model.Image{img1, img2} return imgs, nil } + +func (m *Mock) Health(_ context.Context) (bool, error) { + if m.err != nil { + return false, m.err + } + return true, nil +} diff --git a/domain/service/queue_integration_test.go b/domain/service/queue_integration_test.go index 4bee3b5..e26a5ba 100644 --- a/domain/service/queue_integration_test.go +++ b/domain/service/queue_integration_test.go @@ -1,5 +1,3 @@ -//go:build integration - package service import ( @@ -7,54 +5,46 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitryss/aye-and-nay/infrastructure/cache" + . "github.com/zitryss/aye-and-nay/internal/generator" ) func TestPQueueIntegration(t *testing.T) { - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) + if !*integration { + t.Skip() } - pq := newPQueue(0xFE28, redis) + id, _ := GenId() + redis, err := cache.NewRedis(context.Background(), cache.DefaultRedisConfig) + require.NoError(t, err) + pqueue := id() + album1 := id() + album2 := id() + album3 := id() + pq := newPQueue(pqueue, redis) ctx, cancel := context.WithCancel(context.Background()) defer cancel() pq.Monitor(ctx) go func() { time.Sleep(100 * time.Millisecond) - err := pq.add(ctx, 0x85D5, time.Now().Add(400*time.Millisecond)) - if err != nil { - t.Error(err) - } + err := pq.add(ctx, album1, time.Now().Add(400*time.Millisecond)) + assert.NoError(t, err) time.Sleep(100 * time.Millisecond) - err = pq.add(ctx, 0x89C1, time.Now().Add(200*time.Millisecond)) - if err != nil { - t.Error(err) - } + err = pq.add(ctx, album2, time.Now().Add(200*time.Millisecond)) + assert.NoError(t, err) time.Sleep(100 * time.Millisecond) - err = pq.add(ctx, 0x97D3, time.Now().Add(400*time.Millisecond)) - if err != nil { - t.Error(err) - } + err = pq.add(ctx, album3, time.Now().Add(400*time.Millisecond)) + assert.NoError(t, err) }() album, err := pq.poll(ctx) - if err != nil { - t.Error(err) - } - if album != 0x89C1 { - t.Error("album != 0x89C1") - } + assert.NoError(t, err) + assert.Equal(t, album2, album) album, err = pq.poll(ctx) - if err != nil { - t.Error(err) - } - if album != 0x85D5 { - t.Error("album != 0x85D5") - } + assert.NoError(t, err) + assert.Equal(t, album1, album) album, err = pq.poll(ctx) - if err != nil { - t.Error(err) - } - if album != 0x97D3 { - t.Error("album != 0x97D3") - } + assert.NoError(t, err) + assert.Equal(t, album3, album) } diff --git a/domain/service/queue_test.go b/domain/service/queue_test.go index fedc571..94e72c7 100644 --- a/domain/service/queue_test.go +++ b/domain/service/queue_test.go @@ -1,5 +1,3 @@ -//go:build unit - package service import ( @@ -7,66 +5,53 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/zitryss/aye-and-nay/infrastructure/cache" + . "github.com/zitryss/aye-and-nay/internal/generator" ) func TestPQueue(t *testing.T) { - mem := cache.NewMem() - pq := newPQueue(0xFE28, mem) + if !*unit { + t.Skip() + } + id, _ := GenId() + mem := cache.NewMem(cache.DefaultMemConfig) + pqueue := id() + album1 := id() + album2 := id() + album3 := id() + pq := newPQueue(pqueue, mem) ctx, cancel := context.WithCancel(context.Background()) defer cancel() pq.Monitor(ctx) go func() { time.Sleep(100 * time.Millisecond) - err := pq.add(ctx, 0x85D5, time.Now().Add(400*time.Millisecond)) - if err != nil { - t.Error(err) - } + err := pq.add(ctx, album1, time.Now().Add(400*time.Millisecond)) + assert.NoError(t, err) time.Sleep(100 * time.Millisecond) - err = pq.add(ctx, 0x89C1, time.Now().Add(200*time.Millisecond)) - if err != nil { - t.Error(err) - } + err = pq.add(ctx, album2, time.Now().Add(200*time.Millisecond)) + assert.NoError(t, err) time.Sleep(100 * time.Millisecond) - err = pq.add(ctx, 0x97D3, time.Now().Add(400*time.Millisecond)) - if err != nil { - t.Error(err) - } + err = pq.add(ctx, album3, time.Now().Add(400*time.Millisecond)) + assert.NoError(t, err) }() start := time.Now() album, err := pq.poll(ctx) d := time.Since(start) - if err != nil { - t.Error(err) - } - if album != 0x89C1 { - t.Error("album != 0x89C1") - } - if !(380*time.Millisecond < d && d < 420*time.Millisecond) { - t.Error("!(380*time.Millisecond < d && d < 420*time.Millisecond)") - } + assert.NoError(t, err) + assert.Equal(t, album2, album) + assert.True(t, 380*time.Millisecond < d && d < 420*time.Millisecond) start = time.Now() album, err = pq.poll(ctx) d = time.Since(start) - if err != nil { - t.Error(err) - } - if album != 0x85D5 { - t.Error("album != 0x85D5") - } - if !(80*time.Millisecond < d && d < 120*time.Millisecond) { - t.Error("!(80*time.Millisecond < d && d < 120*time.Millisecond)") - } + assert.NoError(t, err) + assert.Equal(t, album1, album) + assert.True(t, 80*time.Millisecond < d && d < 120*time.Millisecond) start = time.Now() album, err = pq.poll(ctx) d = time.Since(start) - if err != nil { - t.Error(err) - } - if album != 0x97D3 { - t.Error("album != 0x97D3") - } - if !(180*time.Millisecond < d && d < 220*time.Millisecond) { - t.Error("!(180*time.Millisecond < d && d < 220*time.Millisecond)") - } + assert.NoError(t, err) + assert.Equal(t, album3, album) + assert.True(t, 180*time.Millisecond < d && d < 220*time.Millisecond) } diff --git a/domain/service/service.go b/domain/service/service.go index 0fcf1da..8feebb4 100644 --- a/domain/service/service.go +++ b/domain/service/service.go @@ -7,11 +7,17 @@ import ( "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" + "github.com/zitryss/aye-and-nay/pkg/base64" "github.com/zitryss/aye-and-nay/pkg/errors" myrand "github.com/zitryss/aye-and-nay/pkg/rand" ) +var ( + _ domain.Servicer = (*Service)(nil) +) + func New( + conf ServiceConfig, comp domain.Compresser, stor domain.Storager, pers domain.Databaser, @@ -21,7 +27,6 @@ func New( qDel *QueueDel, opts ...options, ) *Service { - conf := newServiceConfig() s := &Service{ conf: conf, comp: comp, @@ -29,6 +34,7 @@ func New( pers: pers, pair: temp, token: temp, + cache: temp, queue: struct { calc *QueueCalc comp *QueueComp @@ -53,7 +59,7 @@ func New( } func NewQueueCalc(q domain.Queuer) *QueueCalc { - return &QueueCalc{newQueue(0x6CF9, q)} + return &QueueCalc{newQueue(0x1, q)} } type QueueCalc struct { @@ -61,7 +67,7 @@ type QueueCalc struct { } func NewQueueComp(q domain.Queuer) *QueueComp { - return &QueueComp{newQueue(0xDD66, q)} + return &QueueComp{newQueue(0x2, q)} } type QueueComp struct { @@ -69,7 +75,7 @@ type QueueComp struct { } func NewQueueDel(q domain.PQueuer) *QueueDel { - return &QueueDel{newPQueue(0xCDF9, q)} + return &QueueDel{newPQueue(0x3, q)} } type QueueDel struct { @@ -90,31 +96,32 @@ func WithRandShuffle(fn func(int, func(int, int))) options { } } -func WithHeartbeatCalc(ch chan<- interface{}) options { +func WithHeartbeatCalc(ch chan<- any) options { return func(s *Service) { s.heartbeat.calc = ch } } -func WithHeartbeatComp(ch chan<- interface{}) options { +func WithHeartbeatComp(ch chan<- any) options { return func(s *Service) { s.heartbeat.comp = ch } } -func WithHeartbeatDel(ch chan<- interface{}) options { +func WithHeartbeatDel(ch chan<- any) options { return func(s *Service) { s.heartbeat.del = ch } } type Service struct { - conf serviceConfig + conf ServiceConfig comp domain.Compresser stor domain.Storager pers domain.Databaser pair domain.Stacker token domain.Tokener + cache domain.Checker queue struct { calc *QueueCalc comp *QueueComp @@ -125,9 +132,9 @@ type Service struct { shuffle func(n int, swap func(i, j int)) } heartbeat struct { - calc chan<- interface{} - comp chan<- interface{} - del chan<- interface{} + calc chan<- any + comp chan<- any + del chan<- any } } @@ -198,33 +205,56 @@ func (s *Service) Pair(ctx context.Context, album uint64) (model.Image, model.Im if err != nil { return model.Image{}, model.Image{}, errors.Wrap(err) } - src1, err := s.pers.GetImageSrc(ctx, album, image1) - if err != nil { - return model.Image{}, model.Image{}, errors.Wrap(err) - } - src2, err := s.pers.GetImageSrc(ctx, album, image2) - if err != nil { - return model.Image{}, model.Image{}, errors.Wrap(err) - } - token1, err := s.rand.id() - if err != nil { - return model.Image{}, model.Image{}, errors.Wrap(err) - } - err = s.token.Set(ctx, album, token1, image1) - if err != nil { - return model.Image{}, model.Image{}, errors.Wrap(err) + src1 := "" + src2 := "" + token1 := image1 + token2 := image2 + if s.conf.TempLinks { + token1, err = s.rand.id() + if err != nil { + return model.Image{}, model.Image{}, errors.Wrap(err) + } + err = s.token.Set(ctx, token1, album, image1) + if err != nil { + return model.Image{}, model.Image{}, errors.Wrap(err) + } + token1B64 := base64.FromUint64(token1) + src1 = "/api/images/" + token1B64 + "/" + token2, err = s.rand.id() + if err != nil { + return model.Image{}, model.Image{}, errors.Wrap(err) + } + err = s.token.Set(ctx, token2, album, image2) + if err != nil { + return model.Image{}, model.Image{}, errors.Wrap(err) + } + token2B64 := base64.FromUint64(token2) + src2 = "/api/images/" + token2B64 + "/" + } else { + src1, err = s.pers.GetImageSrc(ctx, album, image1) + if err != nil { + return model.Image{}, model.Image{}, errors.Wrap(err) + } + src2, err = s.pers.GetImageSrc(ctx, album, image2) + if err != nil { + return model.Image{}, model.Image{}, errors.Wrap(err) + } } - token2, err := s.rand.id() + img1 := model.Image{Id: image1, Src: src1, Token: token1} + img2 := model.Image{Id: image2, Src: src2, Token: token2} + return img1, img2, nil +} + +func (s *Service) Image(ctx context.Context, token uint64) (model.File, error) { + album, image, err := s.token.Get(ctx, token) if err != nil { - return model.Image{}, model.Image{}, errors.Wrap(err) + return model.File{}, errors.Wrap(err) } - err = s.token.Set(ctx, album, token2, image2) + f, err := s.stor.Get(ctx, album, image) if err != nil { - return model.Image{}, model.Image{}, errors.Wrap(err) + return model.File{}, errors.Wrap(err) } - img1 := model.Image{Id: image1, Src: src1, Token: token1} - img2 := model.Image{Id: image2, Src: src2, Token: token2} - return img1, img2, nil + return f, nil } func (s *Service) genPairs(ctx context.Context, album uint64) error { @@ -249,13 +279,26 @@ func (s *Service) genPairs(ctx context.Context, album uint64) error { } func (s *Service) Vote(ctx context.Context, album uint64, tokenFrom uint64, tokenTo uint64) error { - imageFrom, err := s.token.Get(ctx, album, tokenFrom) - if err != nil { - return errors.Wrap(err) - } - imageTo, err := s.token.Get(ctx, album, tokenTo) - if err != nil { - return errors.Wrap(err) + imageFrom := tokenFrom + imageTo := tokenTo + err := error(nil) + if s.conf.TempLinks { + _, imageFrom, err = s.token.Get(ctx, tokenFrom) + if err != nil { + return errors.Wrap(err) + } + err = s.token.Del(ctx, tokenFrom) + if err != nil { + return errors.Wrap(err) + } + _, imageTo, err = s.token.Get(ctx, tokenTo) + if err != nil { + return errors.Wrap(err) + } + err = s.token.Del(ctx, tokenTo) + if err != nil { + return errors.Wrap(err) + } } err = s.pers.SaveVote(ctx, album, imageFrom, imageTo) if err != nil { @@ -276,6 +319,26 @@ func (s *Service) Top(ctx context.Context, album uint64) ([]model.Image, error) return imgs, nil } +func (s *Service) Health(ctx context.Context) (bool, error) { + _, err := s.comp.Health(ctx) + if err != nil { + return false, errors.Wrap(err) + } + _, err = s.stor.Health(ctx) + if err != nil { + return false, errors.Wrap(err) + } + _, err = s.pers.Health(ctx) + if err != nil { + return false, errors.Wrap(err) + } + _, err = s.cache.Health(ctx) + if err != nil { + return false, errors.Wrap(err) + } + return true, nil +} + func (s *Service) CleanUp(ctx context.Context) error { albs, err := s.pers.AlbumsToBeDeleted(ctx) if err != nil { diff --git a/domain/service/service_integration_test.go b/domain/service/service_integration_test.go index 8fb76d7..6b38484 100644 --- a/domain/service/service_integration_test.go +++ b/domain/service/service_integration_test.go @@ -1,741 +1,138 @@ -//go:build integration - package service import ( "context" - "io" - "os" - "reflect" "testing" - "time" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" "golang.org/x/sync/errgroup" - "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/domain/model" "github.com/zitryss/aye-and-nay/infrastructure/cache" "github.com/zitryss/aye-and-nay/infrastructure/compressor" "github.com/zitryss/aye-and-nay/infrastructure/database" "github.com/zitryss/aye-and-nay/infrastructure/storage" - _ "github.com/zitryss/aye-and-nay/internal/config" - "github.com/zitryss/aye-and-nay/internal/dockertest" - . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/env" - "github.com/zitryss/aye-and-nay/pkg/errors" - "github.com/zitryss/aye-and-nay/pkg/log" + . "github.com/zitryss/aye-and-nay/internal/generator" ) -func TestMain(m *testing.M) { - _, err := env.Lookup("CONTINUOUS_INTEGRATION") - if err != nil { - log.SetOutput(os.Stderr) - log.SetLevel(log.Lcritical) - docker := dockertest.New() - docker.RunRedis() - docker.RunImaginary() - docker.RunMongo() - docker.RunMinio() - log.SetOutput(io.Discard) - code := m.Run() - docker.Purge() - os.Exit(code) - } - code := m.Run() - os.Exit(code) +func TestServiceIntegrationTestSuite(t *testing.T) { + suite.Run(t, &ServiceIntegrationTestSuite{}) } -func TestServiceIntegrationAlbum(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x463E + i, nil - } - }() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{newQueue(0xB273, redis)} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - heartbeatComp := make(chan interface{}) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithHeartbeatComp(heartbeatComp)) - gComp, ctxComp := errgroup.WithContext(ctx) - serv.StartWorkingPoolComp(ctxComp, gComp) - files := []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v := CheckChannel(t, heartbeatComp) - p, ok := v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 0.5) { - t.Error("p != 0.5") - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 1) { - t.Error("p != 1") - } - }) - t.Run("Negative", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x915C + i, nil - } - }() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - heartbeatRestart := make(chan interface{}) - comp := compressor.NewShortPixel(compressor.WithHeartbeatRestart(heartbeatRestart)) - comp.Monitor() - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{newQueue(0x88AB, redis)} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - heartbeatComp := make(chan interface{}) - serv := New(comp, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithHeartbeatComp(heartbeatComp)) - gComp, ctxComp := errgroup.WithContext(ctx) - serv.StartWorkingPoolComp(ctxComp, gComp) - files := []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v := CheckChannel(t, heartbeatComp) - _ = CheckChannel(t, heartbeatComp) - err, ok := v.(error) - if !ok { - t.Error("v.(type) != error") - } - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } - files = []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v = CheckChannel(t, heartbeatComp) - p, ok := v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 0.5) { - t.Error("p != 0.5") - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 1) { - t.Error("p != 1") - } - CheckChannel(t, heartbeatRestart) - CheckChannel(t, heartbeatRestart) - files = []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v = CheckChannel(t, heartbeatComp) - _ = CheckChannel(t, heartbeatComp) - err, ok = v.(error) - if !ok { - t.Error("v.(type) != error") - } - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } - files = []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 0.5) { - t.Error("p != 0.5") - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 1) { - t.Error("p != 1") - } - }) +type ServiceIntegrationTestSuite struct { + suite.Suite + base ServiceTestSuite } -func TestServiceIntegrationPair(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x3BC5 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) - files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img7, img8, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x3BC7, Token: 0x3BC9, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/xzsAAAAAAAA"} - img2 := model.Image{Id: 0x3BC8, Token: 0x3BCA, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/yDsAAAAAAAA"} - imgs1 := []model.Image{img1, img2} - if reflect.DeepEqual(img7, img8) { - t.Error("img7 == img8") - } - if !IsIn(img7, imgs1) { - t.Error("img7 is not in imgs") - } - if !IsIn(img8, imgs1) { - t.Error("img8 is not in imgs") - } - img9, img10, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - img3 := model.Image{Id: 0x3BC8, Token: 0x3BCB, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/yDsAAAAAAAA"} - img4 := model.Image{Id: 0x3BC7, Token: 0x3BCC, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/xzsAAAAAAAA"} - imgs2 := []model.Image{img3, img4} - if reflect.DeepEqual(img9, img10) { - t.Error("img9 == img10") - } - if !IsIn(img9, imgs2) { - t.Error("img9 is not in imgs") - } - if !IsIn(img10, imgs2) { - t.Error("img10 is not in imgs") - } - img11, img12, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - img5 := model.Image{Id: 0x3BC7, Token: 0x3BCD, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/xzsAAAAAAAA"} - img6 := model.Image{Id: 0x3BC8, Token: 0x3BCE, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/yDsAAAAAAAA"} - imgs3 := []model.Image{img5, img6} - if reflect.DeepEqual(img11, img12) { - t.Error("img11 == img12") - } - if !IsIn(img11, imgs3) { - t.Error("img11 is not in imgs") - } - if !IsIn(img12, imgs3) { - t.Error("img12 is not in imgs") - } - }) - t.Run("Negative", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel) - _, _, err = serv.Pair(ctx, 0xEB46) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func (suite *ServiceIntegrationTestSuite) SetupSuite() { + if !*integration { + suite.T().Skip() + } + suite.base = ServiceTestSuite{} + suite.base.SetT(suite.T()) + ctx, cancel := context.WithCancel(context.Background()) + comp, err := compressor.New(ctx, compressor.CompressorConfig{Compressor: "mock"}) + require.NoError(suite.T(), err) + stor, err := storage.New(ctx, storage.StorageConfig{Storage: "minio", Minio: storage.DefaultMinioConfig}) + require.NoError(suite.T(), err) + data, err := database.New(ctx, database.DatabaseConfig{Database: "mongo", Mongo: database.DefaultMongoConfig}) + require.NoError(suite.T(), err) + cach, err := cache.New(ctx, cache.CacheConfig{Cache: "redis", Redis: cache.DefaultRedisConfig}) + require.NoError(suite.T(), err) + qCalc := NewQueueCalc(cach) + qCalc.Monitor(ctx) + qComp := NewQueueComp(cach) + qComp.Monitor(ctx) + qDel := NewQueueDel(cach) + qDel.Monitor(ctx) + fnShuffle := func(n int, swap func(i int, j int)) {} + heartbeatComp := make(chan any) + heartbeatCalc := make(chan any) + heartbeatDel := make(chan any) + serv := New(DefaultServiceConfig, comp, stor, data, cach, qCalc, qComp, qDel, + WithRandShuffle(fnShuffle), + WithHeartbeatComp(heartbeatComp), + WithHeartbeatCalc(heartbeatCalc), + WithHeartbeatDel(heartbeatDel), + ) + gComp, ctxComp := errgroup.WithContext(ctx) + serv.StartWorkingPoolComp(ctxComp, gComp) + gCalc, ctxCalc := errgroup.WithContext(ctx) + serv.StartWorkingPoolCalc(ctxCalc, gCalc) + gDel, ctxDel := errgroup.WithContext(ctx) + serv.StartWorkingPoolDel(ctxDel, gDel) + suite.base.ctx = ctx + suite.base.cancel = cancel + suite.base.heartbeatComp = heartbeatComp + suite.base.heartbeatCalc = heartbeatCalc + suite.base.heartbeatDel = heartbeatDel + suite.base.serv = serv + suite.base.gComp = gComp + suite.base.gCalc = gComp + suite.base.gDel = gComp + suite.base.setupTestFn = suite.SetupTest } -func TestServiceIntegrationVote(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0xC389 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) - files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img1, img2, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, img1.Token, img2.Token) - if err != nil { - t.Error(err) - } - }) - t.Run("Negative1", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0xE24F + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) - files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img1, img2, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, 0x12E6, img1.Token, img2.Token) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } - }) - t.Run("Negative2", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0xBC43 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) - files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - _, _, err = serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, 0x1CC1, 0xF83C) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } - }) +func (suite *ServiceIntegrationTestSuite) SetupTest() { + id, ids := GenId() + fnId := func() func() (uint64, error) { + return func() (uint64, error) { + return id(), nil + } + }() + suite.base.id = id + suite.base.ids = ids + suite.base.serv.rand.id = fnId + err := suite.base.serv.stor.(*storage.Minio).Reset() + require.NoError(suite.T(), err) + err = suite.base.serv.pers.(*database.Mongo).Reset() + require.NoError(suite.T(), err) + err = suite.base.serv.cache.(*cache.Redis).Reset() + require.NoError(suite.T(), err) } -func TestServiceIntegrationTop(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x4DB8 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{newQueue(0x1A01, redis)} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - heartbeatCalc := make(chan interface{}) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2), WithHeartbeatCalc(heartbeatCalc)) - gCalc, ctxCalc := errgroup.WithContext(ctx) - serv.StartWorkingPoolCalc(ctxCalc, gCalc) - files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img1, img2, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, img1.Token, img2.Token) - if err != nil { - t.Error(err) - } - CheckChannel(t, heartbeatCalc) - img3, img4, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, img3.Token, img4.Token) - if err != nil { - t.Error(err) - } - CheckChannel(t, heartbeatCalc) - imgs1, err := serv.Top(ctx, album) - if err != nil { - t.Error(err) - } - img5 := model.Image{Id: 0x4DBA, Src: "/aye-and-nay/albums/uU0AAAAAAAA/images/uk0AAAAAAAA", Rating: 0.5, Compressed: false} - img6 := model.Image{Id: 0x4DBB, Src: "/aye-and-nay/albums/uU0AAAAAAAA/images/u00AAAAAAAA", Rating: 0.5, Compressed: false} - imgs2 := []model.Image{img5, img6} - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } - }) - t.Run("Negative", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel) - _, err = serv.Top(ctx, 0x83CD) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func (suite *ServiceIntegrationTestSuite) TearDownTest() { + } -func TestServiceIntegrationDelete(t *testing.T) { - t.Run("Positive1", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{newPQueue(0xE3FF, redis)} - qDel.Monitor(ctx) - alb1 := AlbumEmptyFactory(0x101F) - alb1.Expires = time.Now().Add(-1 * time.Hour) - err = mongo.SaveAlbum(ctx, alb1) - if err != nil { - t.Error(err) - } - alb2 := AlbumEmptyFactory(0xFFBB) - alb2.Expires = time.Now().Add(1 * time.Hour) - err = mongo.SaveAlbum(ctx, alb2) - if err != nil { - t.Error(err) - } - heartbeatDel := make(chan interface{}) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithHeartbeatDel(heartbeatDel)) - err = serv.CleanUp(ctx) - if err != nil { - t.Error(err) - } - gDel, ctxDel := errgroup.WithContext(ctx) - serv.StartWorkingPoolDel(ctxDel, gDel) - v := CheckChannel(t, heartbeatDel) - album, ok := v.(uint64) - if !ok { - t.Error("v.(type) != uint64") - } - if album != 0x101F { - t.Error("album != 0x101F") - } - t.Cleanup(func() { - _ = mongo.DeleteAlbum(context.Background(), 0x101F) - _ = mongo.DeleteAlbum(context.Background(), 0xFFBB) - }) - }) - t.Run("Positive2", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{newPQueue(0xEF3F, redis)} - qDel.Monitor(ctx) - heartbeatDel := make(chan interface{}) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithHeartbeatDel(heartbeatDel)) - gDel, ctxDel := errgroup.WithContext(ctx) - serv.StartWorkingPoolDel(ctxDel, gDel) - files := []model.File{Png(), Png()} - dur := 100 * time.Millisecond - album, err := serv.Album(ctx, files, dur) - if err != nil { - t.Error(err) - } - CheckChannel(t, heartbeatDel) - _, err = serv.Top(ctx, album) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - imaginary, err := compressor.NewImaginary() - if err != nil { - t.Fatal(err) - } - minio, err := storage.NewMinio() - if err != nil { - t.Fatal(err) - } - mongo, err := database.NewMongo() - if err != nil { - t.Fatal(err) - } - redis, err := cache.NewRedis() - if err != nil { - t.Fatal(err) - } - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{newPQueue(0xEF3F, redis)} - qDel.Monitor(ctx) - heartbeatDel := make(chan interface{}) - serv := New(imaginary, minio, mongo, redis, qCalc, qComp, qDel, WithHeartbeatDel(heartbeatDel)) - gDel, ctxDel := errgroup.WithContext(ctx) - serv.StartWorkingPoolDel(ctxDel, gDel) - files := []model.File{Png(), Png()} - dur := 0 * time.Second - album, err := serv.Album(ctx, files, dur) - if err != nil { - t.Error(err) - } - select { - case <-heartbeatDel: - t.Error("<-heartbeatDel") - case <-time.After(1 * time.Second): - } - _, err = serv.Top(ctx, album) - if err != nil { - t.Error(err) - } - }) +func (suite *ServiceIntegrationTestSuite) TearDownSuite() { + suite.base.cancel() + err := suite.base.gDel.Wait() + require.NoError(suite.T(), err) + err = suite.base.gCalc.Wait() + require.NoError(suite.T(), err) + err = suite.base.gComp.Wait() + require.NoError(suite.T(), err) + err = suite.base.serv.stor.(*storage.Minio).Reset() + require.NoError(suite.T(), err) + err = suite.base.serv.pers.(*database.Mongo).Reset() + require.NoError(suite.T(), err) + err = suite.base.serv.cache.(*cache.Redis).Reset() + require.NoError(suite.T(), err) + err = suite.base.serv.pers.(*database.Mongo).Close(suite.base.ctx) + require.NoError(suite.T(), err) + err = suite.base.serv.cache.(*cache.Redis).Close(suite.base.ctx) + require.NoError(suite.T(), err) +} + +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationAlbum() { + suite.base.TestServiceAlbum() +} +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationPair() { + suite.base.TestServicePair() +} +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationImage() { + suite.base.TestServiceImage() +} +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationVote() { + suite.base.TestServiceVote() +} +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationTop() { + suite.base.TestServiceTop() +} +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationDelete() { + suite.base.TestServiceDelete() +} +func (suite *ServiceIntegrationTestSuite) TestServiceIntegrationHealth() { + suite.base.TestServiceHealth() } diff --git a/domain/service/service_test.go b/domain/service/service_test.go index 4471f39..625d7f7 100644 --- a/domain/service/service_test.go +++ b/domain/service/service_test.go @@ -1,13 +1,16 @@ -//go:build unit - package service import ( "context" - "reflect" + "flag" + "io" + "os" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" "golang.org/x/sync/errgroup" "github.com/zitryss/aye-and-nay/domain/domain" @@ -16,557 +19,373 @@ import ( "github.com/zitryss/aye-and-nay/infrastructure/compressor" "github.com/zitryss/aye-and-nay/infrastructure/database" "github.com/zitryss/aye-and-nay/infrastructure/storage" - _ "github.com/zitryss/aye-and-nay/internal/config" + "github.com/zitryss/aye-and-nay/internal/dockertest" + . "github.com/zitryss/aye-and-nay/internal/generator" . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" + "github.com/zitryss/aye-and-nay/pkg/log" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") ) -func TestServiceAlbum(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x463E + i, nil - } - }() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{newQueue(0xB273, mCache)} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - heartbeatComp := make(chan interface{}) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithHeartbeatComp(heartbeatComp)) - gComp, ctxComp := errgroup.WithContext(ctx) - serv.StartWorkingPoolComp(ctxComp, gComp) +func TestMain(m *testing.M) { + flag.Parse() + if *ci || !*integration { + code := m.Run() + os.Exit(code) + } + log.SetOutput(os.Stderr) + log.SetLevel(log.CRITICAL) + docker := dockertest.New() + host := &cache.DefaultRedisConfig.Host + port := &cache.DefaultRedisConfig.Port + docker.RunRedis(host, port) + host = &compressor.DefaultImaginaryConfig.Host + port = &compressor.DefaultImaginaryConfig.Port + docker.RunImaginary(host, port) + host = &database.DefaultMongoConfig.Host + port = &database.DefaultMongoConfig.Port + docker.RunMongo(host, port) + host = &storage.DefaultMinioConfig.Host + port = &storage.DefaultMinioConfig.Port + accessKey := storage.DefaultMinioConfig.AccessKey + secretKey := storage.DefaultMinioConfig.SecretKey + docker.RunMinio(host, port, accessKey, secretKey) + log.SetOutput(io.Discard) + code := m.Run() + docker.Purge() + os.Exit(code) +} + +func TestServiceTestSuite(t *testing.T) { + suite.Run(t, &ServiceTestSuite{}) +} + +type ServiceTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + id IdGenFunc + ids *IdLogBook + heartbeatComp chan any + heartbeatCalc chan any + heartbeatDel chan any + serv *Service + gComp *errgroup.Group + gCalc *errgroup.Group + gDel *errgroup.Group + setupTestFn func() +} + +func (suite *ServiceTestSuite) SetupSuite() { + if !*unit { + suite.T().Skip() + } + ctx, cancel := context.WithCancel(context.Background()) + comp := compressor.NewMock() + stor := storage.NewMock() + data := database.NewMem(database.DefaultMemConfig) + cach := cache.NewMem(cache.DefaultMemConfig) + qCalc := NewQueueCalc(cach) + qCalc.Monitor(ctx) + qComp := NewQueueComp(cach) + qComp.Monitor(ctx) + qDel := NewQueueDel(cach) + qDel.Monitor(ctx) + fnShuffle := func(n int, swap func(i int, j int)) {} + heartbeatComp := make(chan any) + heartbeatCalc := make(chan any) + heartbeatDel := make(chan any) + serv := New(DefaultServiceConfig, comp, stor, data, cach, qCalc, qComp, qDel, + WithRandShuffle(fnShuffle), + WithHeartbeatComp(heartbeatComp), + WithHeartbeatCalc(heartbeatCalc), + WithHeartbeatDel(heartbeatDel), + ) + gComp, ctxComp := errgroup.WithContext(ctx) + serv.StartWorkingPoolComp(ctxComp, gComp) + gCalc, ctxCalc := errgroup.WithContext(ctx) + serv.StartWorkingPoolCalc(ctxCalc, gCalc) + gDel, ctxDel := errgroup.WithContext(ctx) + serv.StartWorkingPoolDel(ctxDel, gDel) + suite.ctx = ctx + suite.cancel = cancel + suite.heartbeatComp = heartbeatComp + suite.heartbeatCalc = heartbeatCalc + suite.heartbeatDel = heartbeatDel + suite.serv = serv + suite.gComp = gComp + suite.gCalc = gComp + suite.gDel = gComp + suite.setupTestFn = suite.SetupTest +} + +func (suite *ServiceTestSuite) SetupTest() { + id, ids := GenId() + fnId := func() func() (uint64, error) { + return func() (uint64, error) { + return id(), nil + } + }() + suite.id = id + suite.ids = ids + suite.serv.rand.id = fnId + err := suite.serv.pers.(*database.Mem).Reset() + require.NoError(suite.T(), err) + err = suite.serv.cache.(*cache.Mem).Reset() + require.NoError(suite.T(), err) +} + +func (suite *ServiceTestSuite) TearDownTest() { + +} + +func (suite *ServiceTestSuite) TearDownSuite() { + suite.cancel() + err := suite.gDel.Wait() + require.NoError(suite.T(), err) + err = suite.gCalc.Wait() + require.NoError(suite.T(), err) + err = suite.gComp.Wait() + require.NoError(suite.T(), err) + err = suite.serv.pers.(*database.Mem).Reset() + require.NoError(suite.T(), err) + err = suite.serv.cache.(*cache.Mem).Reset() + require.NoError(suite.T(), err) +} + +func (suite *ServiceTestSuite) TestServiceAlbum() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} - _, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v := CheckChannel(t, heartbeatComp) + _, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + v := AssertChannel(t, suite.heartbeatComp) p, ok := v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 0.5) { - t.Error("p != 0.5") - } - v = CheckChannel(t, heartbeatComp) + assert.True(t, ok) + assert.InDelta(t, 0.5, p, TOLERANCE) + v = AssertChannel(t, suite.heartbeatComp) p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 1) { - t.Error("p != 1") - } - }) - t.Run("Negative", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x915C + i, nil - } - }() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - heartbeatRestart := make(chan interface{}) - comp := compressor.NewShortPixel(compressor.WithHeartbeatRestart(heartbeatRestart)) - comp.Monitor() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{newQueue(0x88AB, mCache)} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - heartbeatComp := make(chan interface{}) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithHeartbeatComp(heartbeatComp)) - gComp, ctxComp := errgroup.WithContext(ctx) - serv.StartWorkingPoolComp(ctxComp, gComp) - files := []model.File{Png(), Png()} - _, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v := CheckChannel(t, heartbeatComp) - _ = CheckChannel(t, heartbeatComp) - err, ok := v.(error) - if !ok { - t.Error("v.(type) != error") - } - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } - files = []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v = CheckChannel(t, heartbeatComp) - p, ok := v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 0.5) { - t.Error("p != 0.5") - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 1) { - t.Error("p != 1") - } - CheckChannel(t, heartbeatRestart) - CheckChannel(t, heartbeatRestart) - files = []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v = CheckChannel(t, heartbeatComp) - _ = CheckChannel(t, heartbeatComp) - err, ok = v.(error) - if !ok { - t.Error("v.(type) != error") - } - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } - files = []model.File{Png(), Png()} - _, err = serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 0.5) { - t.Error("p != 0.5") - } - v = CheckChannel(t, heartbeatComp) - p, ok = v.(float64) - if !ok { - t.Error("v.(type) != float64") - } - if !EqualFloat(p, 1) { - t.Error("p != 1") - } + assert.True(t, ok) + assert.InDelta(t, 1, p, TOLERANCE) }) } -func TestServicePair(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x3BC5 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) +func (suite *ServiceTestSuite) TestServicePair() { + suite.T().Run("Positive1", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img7, img8, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x3BC7, Token: 0x3BC9, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/xzsAAAAAAAA"} - img2 := model.Image{Id: 0x3BC8, Token: 0x3BCA, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/yDsAAAAAAAA"} + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img7, img8, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + img1 := model.Image{Id: suite.ids.Uint64(1), Token: suite.ids.Uint64(3), Src: "/api/images/" + suite.ids.Base64(3) + "/"} + img2 := model.Image{Id: suite.ids.Uint64(2), Token: suite.ids.Uint64(4), Src: "/api/images/" + suite.ids.Base64(4) + "/"} imgs1 := []model.Image{img1, img2} - if reflect.DeepEqual(img7, img8) { - t.Error("img7 == img8") - } - if !IsIn(img7, imgs1) { - t.Error("img7 is not in imgs") - } - if !IsIn(img8, imgs1) { - t.Error("img8 is not in imgs") - } - img9, img10, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - img3 := model.Image{Id: 0x3BC8, Token: 0x3BCB, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/yDsAAAAAAAA"} - img4 := model.Image{Id: 0x3BC7, Token: 0x3BCC, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/xzsAAAAAAAA"} + assert.NotEqual(t, img7, img8) + assert.Contains(t, imgs1, img7) + assert.Contains(t, imgs1, img8) + img9, img10, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + img3 := model.Image{Id: suite.ids.Uint64(2), Token: suite.ids.Uint64(5), Src: "/api/images/" + suite.ids.Base64(5) + "/"} + img4 := model.Image{Id: suite.ids.Uint64(1), Token: suite.ids.Uint64(6), Src: "/api/images/" + suite.ids.Base64(6) + "/"} imgs2 := []model.Image{img3, img4} - if reflect.DeepEqual(img9, img10) { - t.Error("img9 == img10") - } - if !IsIn(img9, imgs2) { - t.Error("img9 is not in imgs") - } - if !IsIn(img10, imgs2) { - t.Error("img10 is not in imgs") - } - img11, img12, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - img5 := model.Image{Id: 0x3BC7, Token: 0x3BCD, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/xzsAAAAAAAA"} - img6 := model.Image{Id: 0x3BC8, Token: 0x3BCE, Src: "/aye-and-nay/albums/xjsAAAAAAAA/images/yDsAAAAAAAA"} + assert.NotEqual(t, img9, img10) + assert.Contains(t, imgs2, img9) + assert.Contains(t, imgs2, img10) + img11, img12, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + img5 := model.Image{Id: suite.ids.Uint64(1), Token: suite.ids.Uint64(7), Src: "/api/images/" + suite.ids.Base64(7) + "/"} + img6 := model.Image{Id: suite.ids.Uint64(2), Token: suite.ids.Uint64(8), Src: "/api/images/" + suite.ids.Base64(8) + "/"} imgs3 := []model.Image{img5, img6} - if reflect.DeepEqual(img11, img12) { - t.Error("img11 == img12") - } - if !IsIn(img11, imgs3) { - t.Error("img11 is not in imgs") - } - if !IsIn(img12, imgs3) { - t.Error("img12 is not in imgs") - } + assert.NotEqual(t, img11, img12) + assert.Contains(t, imgs3, img11) + assert.Contains(t, imgs3, img12) }) - t.Run("Negative", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel) - _, _, err := serv.Pair(ctx, 0xEB46) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Positive2", func(t *testing.T) { + suite.setupTestFn() + oldTempLinks := suite.serv.conf.TempLinks + suite.serv.conf.TempLinks = false + defer func() { suite.serv.conf.TempLinks = oldTempLinks }() + files := []model.File{Png(), Png()} + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img7, img8, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + img1 := model.Image{Id: suite.ids.Uint64(1), Token: suite.ids.Uint64(1), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(1)} + img2 := model.Image{Id: suite.ids.Uint64(2), Token: suite.ids.Uint64(2), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(2)} + imgs1 := []model.Image{img1, img2} + assert.NotEqual(t, img7, img8) + assert.Contains(t, imgs1, img7) + assert.Contains(t, imgs1, img8) + img9, img10, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + img3 := model.Image{Id: suite.ids.Uint64(2), Token: suite.ids.Uint64(2), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(2)} + img4 := model.Image{Id: suite.ids.Uint64(1), Token: suite.ids.Uint64(1), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(1)} + imgs2 := []model.Image{img3, img4} + assert.NotEqual(t, img9, img10) + assert.Contains(t, imgs2, img9) + assert.Contains(t, imgs2, img10) + img11, img12, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + img5 := model.Image{Id: suite.ids.Uint64(1), Token: suite.ids.Uint64(1), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(1)} + img6 := model.Image{Id: suite.ids.Uint64(2), Token: suite.ids.Uint64(2), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(2)} + imgs3 := []model.Image{img5, img6} + assert.NotEqual(t, img11, img12) + assert.Contains(t, imgs3, img11) + assert.Contains(t, imgs3, img12) + }) + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + _, _, err := suite.serv.Pair(suite.ctx, suite.id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } -func TestServiceVote(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0xC389 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) +func (suite *ServiceTestSuite) TestServiceImage() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img1, img2, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, img1.Token, img2.Token) - if err != nil { - t.Error(err) - } + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img1, img2, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + f, err := suite.serv.Image(suite.ctx, img1.Token) + assert.NoError(t, err) + assert.NotNil(t, f.Reader) + f, err = suite.serv.Image(suite.ctx, img2.Token) + assert.NoError(t, err) + assert.NotNil(t, f.Reader) }) - t.Run("Negative1", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0xE24F + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + _, err := suite.serv.Image(suite.ctx, suite.id()) + assert.ErrorIs(t, err, domain.ErrTokenNotFound) + }) +} + +func (suite *ServiceTestSuite) TestServiceVote() { + suite.T().Run("Positive1", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img1, img2, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, 0x12E6, img1.Token, img2.Token) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img1, img2, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + err = suite.serv.Vote(suite.ctx, album, img1.Token, img2.Token) + assert.NoError(t, err) }) - t.Run("Negative2", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0xBC43 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2)) + suite.T().Run("Positive2", func(t *testing.T) { + suite.setupTestFn() + oldTempLinks := suite.serv.conf.TempLinks + suite.serv.conf.TempLinks = false + defer func() { suite.serv.conf.TempLinks = oldTempLinks }() files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - _, _, err = serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, 0x1CC1, 0xF83C) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img1, img2, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + err = suite.serv.Vote(suite.ctx, album, img1.Token, img2.Token) + assert.NoError(t, err) + }) + suite.T().Run("Negative1", func(t *testing.T) { + suite.setupTestFn() + files := []model.File{Png(), Png()} + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img1, img2, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + err = suite.serv.Vote(suite.ctx, suite.id(), img1.Token, img2.Token) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) + }) + suite.T().Run("Negative2", func(t *testing.T) { + suite.setupTestFn() + files := []model.File{Png(), Png()} + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + _, _, err = suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + err = suite.serv.Vote(suite.ctx, album, suite.id(), suite.id()) + assert.ErrorIs(t, err, domain.ErrTokenNotFound) }) } -func TestServiceTop(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - fn1 := func() func() (uint64, error) { - i := uint64(0) - return func() (uint64, error) { - i++ - return 0x4DB8 + i, nil - } - }() - fn2 := func(n int, swap func(i int, j int)) { - } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{newQueue(0x1A01, mCache)} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - heartbeatCalc := make(chan interface{}) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithRandId(fn1), WithRandShuffle(fn2), WithHeartbeatCalc(heartbeatCalc)) - gCalc, ctxCalc := errgroup.WithContext(ctx) - serv.StartWorkingPoolCalc(ctxCalc, gCalc) +func (suite *ServiceTestSuite) TestServiceTop() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} - album, err := serv.Album(ctx, files, 0*time.Millisecond) - if err != nil { - t.Error(err) - } - img1, img2, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, img1.Token, img2.Token) - if err != nil { - t.Error(err) - } - CheckChannel(t, heartbeatCalc) - img3, img4, err := serv.Pair(ctx, album) - if err != nil { - t.Error(err) - } - err = serv.Vote(ctx, album, img3.Token, img4.Token) - if err != nil { - t.Error(err) - } - CheckChannel(t, heartbeatCalc) - imgs1, err := serv.Top(ctx, album) - if err != nil { - t.Error(err) - } - img5 := model.Image{Id: 0x4DBA, Src: "/aye-and-nay/albums/uU0AAAAAAAA/images/uk0AAAAAAAA", Rating: 0.5, Compressed: false} - img6 := model.Image{Id: 0x4DBB, Src: "/aye-and-nay/albums/uU0AAAAAAAA/images/u00AAAAAAAA", Rating: 0.5, Compressed: false} + album, err := suite.serv.Album(suite.ctx, files, 0*time.Millisecond) + assert.NoError(t, err) + img1, img2, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + err = suite.serv.Vote(suite.ctx, album, img1.Token, img2.Token) + assert.NoError(t, err) + AssertChannel(t, suite.heartbeatCalc) + img3, img4, err := suite.serv.Pair(suite.ctx, album) + assert.NoError(t, err) + err = suite.serv.Vote(suite.ctx, album, img3.Token, img4.Token) + assert.NoError(t, err) + AssertChannel(t, suite.heartbeatCalc) + imgs1, err := suite.serv.Top(suite.ctx, album) + assert.NoError(t, err) + img5 := model.Image{Id: suite.ids.Uint64(1), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(1), Rating: 0.5, Compressed: false} + img6 := model.Image{Id: suite.ids.Uint64(2), Src: "/aye-and-nay/albums/" + suite.ids.Base64(0) + "/images/" + suite.ids.Base64(2), Rating: 0.5, Compressed: false} imgs2 := []model.Image{img5, img6} - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } + assert.Equal(t, imgs2, imgs1) }) - t.Run("Negative", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{} - qDel.Monitor(ctx) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel) - _, err := serv.Top(ctx, 0x83CD) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + _, err := suite.serv.Top(suite.ctx, suite.id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } -func TestServiceDelete(t *testing.T) { - t.Run("Positive1", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{newPQueue(0xE3FF, mCache)} - qDel.Monitor(ctx) - alb1 := AlbumEmptyFactory(0x101F) +func (suite *ServiceTestSuite) TestServiceDelete() { + suite.T().Run("Positive1", func(t *testing.T) { + suite.setupTestFn() + id1, ids1 := GenId() + alb1 := AlbumFactory(id1, ids1) alb1.Expires = time.Now().Add(-1 * time.Hour) - err := mDb.SaveAlbum(ctx, alb1) - if err != nil { - t.Error(err) - } - alb2 := AlbumEmptyFactory(0xFFBB) + err := suite.serv.pers.SaveAlbum(suite.ctx, alb1) + assert.NoError(t, err) + id2, ids2 := GenId() + alb2 := AlbumFactory(id2, ids2) alb2.Expires = time.Now().Add(1 * time.Hour) - err = mDb.SaveAlbum(ctx, alb2) - if err != nil { - t.Error(err) - } - heartbeatDel := make(chan interface{}) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithHeartbeatDel(heartbeatDel)) - err = serv.CleanUp(ctx) - if err != nil { - t.Error(err) - } - gDel, ctxDel := errgroup.WithContext(ctx) - serv.StartWorkingPoolDel(ctxDel, gDel) - v := CheckChannel(t, heartbeatDel) + err = suite.serv.pers.SaveAlbum(suite.ctx, alb2) + assert.NoError(t, err) + err = suite.serv.CleanUp(suite.ctx) + assert.NoError(t, err) + v := AssertChannel(t, suite.heartbeatDel) album, ok := v.(uint64) - if !ok { - t.Error("v.(type) != uint64") - } - if album != 0x101F { - t.Error("album != 0x101F") - } + assert.True(t, ok) + assert.Equal(t, ids1.Uint64(0), album) }) - t.Run("Positive2", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{newPQueue(0xEF3F, mCache)} - qDel.Monitor(ctx) - heartbeatDel := make(chan interface{}) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithHeartbeatDel(heartbeatDel)) - gDel, ctxDel := errgroup.WithContext(ctx) - serv.StartWorkingPoolDel(ctxDel, gDel) + suite.T().Run("Positive2", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} dur := 100 * time.Millisecond - album, err := serv.Album(ctx, files, dur) - if err != nil { - t.Error(err) - } - CheckChannel(t, heartbeatDel) - _, err = serv.Top(ctx, album) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + album, err := suite.serv.Album(suite.ctx, files, dur) + assert.NoError(t, err) + AssertChannel(t, suite.heartbeatDel) + _, err = suite.serv.Top(suite.ctx, album) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - comp := compressor.NewMock() - stor := storage.NewMock() - mDb := database.NewMem() - mCache := cache.NewMem() - qCalc := &QueueCalc{} - qCalc.Monitor(ctx) - qComp := &QueueComp{} - qComp.Monitor(ctx) - qDel := &QueueDel{newPQueue(0xEF3F, mCache)} - qDel.Monitor(ctx) - heartbeatDel := make(chan interface{}) - serv := New(comp, stor, mDb, mCache, qCalc, qComp, qDel, WithHeartbeatDel(heartbeatDel)) - gDel, ctxDel := errgroup.WithContext(ctx) - serv.StartWorkingPoolDel(ctxDel, gDel) + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() files := []model.File{Png(), Png()} dur := 0 * time.Second - album, err := serv.Album(ctx, files, dur) - if err != nil { - t.Error(err) - } - select { - case <-heartbeatDel: - t.Error("<-heartbeatDel") - case <-time.After(1 * time.Second): - } - _, err = serv.Top(ctx, album) - if err != nil { - t.Error(err) - } + album, err := suite.serv.Album(suite.ctx, files, dur) + assert.NoError(t, err) + AssertNotChannel(t, suite.heartbeatDel) + _, err = suite.serv.Top(suite.ctx, album) + assert.NoError(t, err) }) } + +func (suite *ServiceTestSuite) TestServiceHealth() { + _, err := suite.serv.Health(suite.ctx) + assert.NoError(suite.T(), err) +} diff --git a/domain/service/workerpool.go b/domain/service/workerpool.go index c761aac..4a46286 100644 --- a/domain/service/workerpool.go +++ b/domain/service/workerpool.go @@ -12,7 +12,7 @@ import ( func (s *Service) StartWorkingPoolCalc(ctx context.Context, g *errgroup.Group) { go func() { - sem := make(chan struct{}, s.conf.numberOfWorkersCalc) + sem := make(chan struct{}, s.conf.NumberOfWorkersCalc) for { select { case sem <- struct{}{}: @@ -58,7 +58,7 @@ func (s *Service) StartWorkingPoolCalc(ctx context.Context, g *errgroup.Group) { e = err continue } - vect := linalg.PageRank(edgs, s.conf.accuracy) + vect := linalg.PageRank(edgs, s.conf.Accuracy) err = s.pers.UpdateRatings(ctx, album, vect) if err != nil { err = errors.Wrap(err) @@ -67,7 +67,11 @@ func (s *Service) StartWorkingPoolCalc(ctx context.Context, g *errgroup.Group) { continue } if s.heartbeat.calc != nil { - s.heartbeat.calc <- struct{}{} + select { + case <-ctx.Done(): + return + case s.heartbeat.calc <- struct{}{}: + } } } }) @@ -77,7 +81,7 @@ func (s *Service) StartWorkingPoolCalc(ctx context.Context, g *errgroup.Group) { func (s *Service) StartWorkingPoolComp(ctx context.Context, g *errgroup.Group) { go func() { - sem := make(chan struct{}, s.conf.numberOfWorkersComp) + sem := make(chan struct{}, s.conf.NumberOfWorkersComp) for { select { case sem <- struct{}{}: @@ -134,7 +138,11 @@ func (s *Service) StartWorkingPoolComp(ctx context.Context, g *errgroup.Group) { f, err = s.comp.Compress(ctx, f) if errors.Is(err, domain.ErrThirdPartyUnavailable) { if s.heartbeat.comp != nil { - s.heartbeat.comp <- err + select { + case <-ctx.Done(): + return + case s.heartbeat.comp <- err: + } } } if err != nil { @@ -159,7 +167,11 @@ func (s *Service) StartWorkingPoolComp(ctx context.Context, g *errgroup.Group) { } if s.heartbeat.comp != nil { p, _ := s.Progress(ctx, album) - s.heartbeat.comp <- p + select { + case <-ctx.Done(): + return + case s.heartbeat.comp <- p: + } } } } @@ -224,7 +236,11 @@ func (s *Service) StartWorkingPoolDel(ctx context.Context, g *errgroup.Group) { } } if s.heartbeat.del != nil { - s.heartbeat.del <- album + select { + case <-ctx.Done(): + return + case s.heartbeat.del <- album: + } } } }) diff --git a/go.mod b/go.mod index 3ff8427..e67a1d2 100644 --- a/go.mod +++ b/go.mod @@ -1,102 +1,118 @@ module github.com/zitryss/aye-and-nay -go 1.17 +go 1.19 require ( - github.com/caddyserver/certmagic v0.15.2 - github.com/cheggaaa/pb/v3 v3.0.8 - github.com/dgraph-io/badger/v3 v3.2103.2 - github.com/emirpasic/gods v1.12.0 - github.com/go-redis/redis/v8 v8.11.4 + github.com/caddyserver/certmagic v0.17.2 + github.com/dgraph-io/badger/v3 v3.2103.3 + github.com/emirpasic/gods v1.18.1 + github.com/go-playground/validator v9.31.0+incompatible + github.com/go-redis/redis/v8 v8.11.5 + github.com/go-redis/redis_rate/v9 v9.1.2 github.com/hashicorp/golang-lru v0.5.4 github.com/julienschmidt/httprouter v1.3.0 github.com/mailru/easyjson v0.7.7 - github.com/minio/minio-go/v7 v7.0.16 - github.com/ory/dockertest/v3 v3.6.5 - github.com/rs/cors v1.8.0 - github.com/spf13/viper v1.9.0 - go.mongodb.org/mongo-driver v1.8.0 - golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 - golang.org/x/tools v0.1.7 + github.com/minio/minio-go/v7 v7.0.43 + github.com/ory/dockertest/v3 v3.9.1 + github.com/radovskyb/watcher v1.0.7 + github.com/rs/cors v1.8.2 + github.com/segmentio/asm v1.2.0 + github.com/shirou/gopsutil v3.21.11+incompatible + github.com/spf13/afero v1.9.2 + github.com/spf13/viper v1.13.0 + github.com/stretchr/testify v1.8.1 + go.mongodb.org/mongo-driver v1.10.4 + go.uber.org/atomic v1.10.0 + go.uber.org/zap v1.23.0 + golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 + golang.org/x/net v0.1.0 + golang.org/x/sync v0.1.0 + golang.org/x/time v0.1.0 + golang.org/x/tools v0.2.0 ) require ( - github.com/Azure/go-ansiterm v0.0.0-20210608035416-43c61cb656b4 // indirect - github.com/Microsoft/go-winio v0.5.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect - github.com/VividCortex/ewma v1.2.0 // indirect - github.com/cenkalti/backoff/v4 v4.1.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/containerd/continuity v0.1.0 // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/docker/cli v20.10.21+incompatible // indirect + github.com/docker/docker v20.10.21+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect - github.com/docker/go-units v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect - github.com/fatih/color v1.12.0 // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/go-stack/stack v1.8.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-playground/locales v0.14.0 // indirect + github.com/go-playground/universal-translator v0.18.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/flatbuffers v2.0.0+incompatible // indirect + github.com/google/flatbuffers v22.10.26+incompatible // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.13.6 // indirect - github.com/klauspost/cpuid/v2 v2.0.9 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/klauspost/compress v1.15.12 // indirect + github.com/klauspost/cpuid/v2 v2.1.2 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/lib/pq v1.8.0 // indirect github.com/libdns/libdns v0.2.1 // indirect - github.com/magiconair/properties v1.8.5 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect - github.com/mattn/go-isatty v0.0.13 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/mholt/acmez v1.0.1 // indirect - github.com/miekg/dns v1.1.43 // indirect + github.com/magiconair/properties v1.8.6 // indirect + github.com/mholt/acmez v1.0.4 // indirect + github.com/miekg/dns v1.1.50 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.4.3 // indirect - github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.6.6 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.1 // indirect - github.com/opencontainers/runc v1.0.0-rc95 // indirect - github.com/pelletier/go-toml v1.9.4 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.4 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/rivo/uniseg v0.2.0 // indirect - github.com/rs/xid v1.3.0 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect - github.com/spf13/afero v1.6.0 // indirect - github.com/spf13/cast v1.4.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.2.0 // indirect + github.com/subosito/gotenv v1.4.1 // indirect github.com/tidwall/pretty v1.0.2 // indirect + github.com/tklauser/go-sysconf v0.3.10 // indirect + github.com/tklauser/numcpus v0.5.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.0.2 // indirect - github.com/xdg-go/stringprep v1.0.2 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.7.0 // indirect - go.uber.org/zap v1.19.1 // indirect - golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect - golang.org/x/mod v0.5.1 // indirect - golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/protobuf v1.27.1 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/sys v0.1.0 // indirect + golang.org/x/text v0.4.0 // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect - gopkg.in/ini.v1 v1.66.2 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/ini.v1 v1.67.0 // 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 e58be76..e4d253a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -16,14 +16,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -32,7 +25,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -42,143 +34,120 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210608035416-43c61cb656b4 h1:poX3j1kSFMgZhtUGrKSAwjh/FKVYrzvoXzwyXPHkAv0= -github.com/Azure/go-ansiterm v0.0.0-20210608035416-43c61cb656b4/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= -github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= -github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/caddyserver/certmagic v0.15.2 h1:OMTakTsLM1ZfzMDjwvYprfUgFzpVPh3u87oxMPwmeBc= -github.com/caddyserver/certmagic v0.15.2/go.mod h1:qhkAOthf72ufAcp3Y5jF2RaGE96oip3UbEQRIzwe3/8= -github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= -github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= +github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= -github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA= -github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= -github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.1.0 h1:UFRRY5JemiAhPZrr/uE0n8fMTLcZsUvySPr1+D7pgr8= -github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.1/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= +github.com/dgraph-io/badger/v3 v3.2103.3 h1:s63J1pisDhKpzWslXFe+ChuthuZptpwTE6qEKoczPb4= +github.com/dgraph-io/badger/v3 v3.2103.3/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/cli v20.10.18+incompatible h1:f/GQLsVpo10VvToRay2IraVA1wHz9KktZyjev3SIVDU= +github.com/docker/cli v20.10.18+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.21+incompatible h1:qVkgyYUnOLQ98LtXBrwd/duVqPT2X4SHndOuGsfwyhU= +github.com/docker/cli v20.10.21+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v20.10.18+incompatible h1:SN84VYXTBNGn92T/QwIRPlum9zfemfitN7pbsp26WSc= +github.com/docker/docker v20.10.18+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.21+incompatible h1:UTLdBmHk3bEY+w8qeO5KttOhy6OmXWsl/FEet9Uswog= +github.com/docker/docker v20.10.21+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= -github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= -github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= -github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= -github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-redis/redis_rate/v9 v9.1.2 h1:H0l5VzoAtOE6ydd38j8MCq3ABlGLnvvbA1xDSVVCHgQ= +github.com/go-redis/redis_rate/v9 v9.1.2/go.mod h1:oam2de2apSgRG8aJzwJddXbNu91Iyz1m8IKJE2vpvlQ= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -191,8 +160,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -208,7 +175,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -218,8 +184,10 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI= -github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v22.9.29+incompatible h1:3UBb679lq3V/O9rgzoJmnkP1jJzmC9OdFzITUBkLU/A= +github.com/google/flatbuffers v22.9.29+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v22.10.26+incompatible h1:z1QiaMyPu1x3Z6xf2u1dsLj1ZxicdGSeaLpCuIsQNZM= +github.com/google/flatbuffers v22.10.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -231,13 +199,11 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -247,243 +213,149 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= -github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= -github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/klauspost/cpuid/v2 v2.1.2 h1:XhdX4fqAJUA0yj+kUwMavO0hHrSPAecYdYf1ZmxHvak= +github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= -github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mholt/acmez v1.0.1 h1:J7uquHOKEmo71UDnVApy1sSLA0oF/r+NtVrNzMKKA9I= -github.com/mholt/acmez v1.0.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= +github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.16 h1:GspaSBS8lOuEUCAqMe0W3UxSoyOA4b4F8PTspRVI+k4= -github.com/minio/minio-go/v7 v7.0.16/go.mod h1:pUV0Pc+hPd1nccgmzQF/EXh48l/Z/yps6QPF1aaie4g= -github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= +github.com/minio/minio-go/v7 v7.0.40 h1:dgyyRKelGW1B/7spyDyvHv9LI3RK5AJDJUrIRllyLk4= +github.com/minio/minio-go/v7 v7.0.40/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= +github.com/minio/minio-go/v7 v7.0.43 h1:14Q4lwblqTdlAmba05oq5xL0VBLHi06zS4yLnIkz6hI= +github.com/minio/minio-go/v7 v7.0.43/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASMg2/nvmbarw= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= -github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= -github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI= +github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 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.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v1.0.0-rc95 h1:RMuWVfY3E1ILlVsC3RhIq38n4sJtlOFwU9gfFZSqrd0= -github.com/opencontainers/runc v1.0.0-rc95/go.mod h1:z+bZxa/+Tz/FmYVWkhUajJdzFeOqjc5vrqskhVyHGUM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg= +github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= -github.com/ory/dockertest/v3 v3.6.5 h1:mhNKFeVEHuvaYW+/u+59mLzM/6XXGjpaet/yApgv+yc= -github.com/ory/dockertest/v3 v3.6.5/go.mod h1:iYKQSRlYrt/2s5fJWYdB98kCQG6g/LjBMvzEYii63vg= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= +github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= -github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= +github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -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/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= +github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= -github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYIR88KRMEuODE= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= +github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= -github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -491,40 +363,50 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= -github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= +github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= +github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= +github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= +github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/tklauser/numcpus v0.5.0 h1:ooe7gN0fg6myJ0EKoTAf5hebTZrH52px3New/D9iJ+A= +github.com/tklauser/numcpus v0.5.0/go.mod h1:OGzpTxpcIMNGYQdit2BYL1pvk/dSOaJWjKoflh+RQjo= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= -github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= 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.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= -github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= -github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc= -github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= @@ -534,13 +416,12 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.mongodb.org/mongo-driver v1.8.0 h1:R/P/JJzu8LJvJ1lDfph9GLNIKQxEtIHFfnUUUve35zY= -go.mongodb.org/mongo-driver v1.8.0/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.10.3 h1:XDQEvmh6z1EUsXuIkXE9TaVeqHw6SwS1uf93jFs0HBA= +go.mongodb.org/mongo-driver v1.10.3/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= +go.mongodb.org/mongo-driver v1.10.4 h1:taPWsSsfn723M05lMyd/TAQe0kU9PsEYQ15WslnBtQw= +go.mongodb.org/mongo-driver v1.10.4/go.mod h1:z4XpeoU6w+9Vht+jAFyLgVrD+jGSQQe0+CBWFHNiHt8= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -549,41 +430,31 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= -go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2 h1:x8vtB3zMecnlqZIwJNUUpwYKYSqCz5jXbiyv0ZJJZeI= +golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -594,6 +465,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20221011111909-0220f59fc3e4 h1:vJ72ext0XgGVpu8rmB7rsWFCFaSMeygqU5ewGUzYdNs= +golang.org/x/exp v0.0.0-20221011111909-0220f59fc3e4/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE= +golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -607,7 +482,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -619,27 +493,22 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/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-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -650,7 +519,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -660,17 +528,15 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c= -golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4= +golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -680,12 +546,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -696,43 +556,30 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -743,77 +590,72 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14 h1:k5II8e6QD8mITdi+okbbmR/cIyEbeXLBhy5Ha4nevyc= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= -golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -844,21 +686,19 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -879,14 +719,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -917,7 +749,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -929,26 +760,10 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= @@ -960,19 +775,9 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -985,40 +790,31 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= -gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= -gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= -gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/infrastructure/cache/cache.go b/infrastructure/cache/cache.go index 14b62e9..5659c92 100644 --- a/infrastructure/cache/cache.go +++ b/infrastructure/cache/cache.go @@ -1,22 +1,24 @@ package cache import ( + "context" + "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/pkg/log" + "github.com/zitryss/aye-and-nay/internal/log" ) -func New(s string) (domain.Cacher, error) { - switch s { +func New(ctx context.Context, conf CacheConfig) (domain.Cacher, error) { + switch conf.Cache { case "redis": - log.Info("connecting to cache") - return NewRedis() + log.Info(context.Background(), "connecting to cache") + return NewRedis(ctx, conf.Redis) case "mem": - mem := NewMem() - mem.Monitor() + mem := NewMem(conf.Mem) + mem.Monitor(ctx) return mem, nil default: - mem := NewMem() - mem.Monitor() + mem := NewMem(conf.Mem) + mem.Monitor(ctx) return mem, nil } } diff --git a/infrastructure/cache/cache_integration_test.go b/infrastructure/cache/cache_integration_test.go deleted file mode 100644 index 8f9231d..0000000 --- a/infrastructure/cache/cache_integration_test.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build integration - -package cache - -import ( - "io" - "os" - "testing" - - "github.com/zitryss/aye-and-nay/internal/dockertest" - "github.com/zitryss/aye-and-nay/pkg/env" - "github.com/zitryss/aye-and-nay/pkg/log" -) - -func TestMain(m *testing.M) { - _, err := env.Lookup("CONTINUOUS_INTEGRATION") - if err != nil { - log.SetOutput(os.Stderr) - log.SetLevel(log.Lcritical) - docker := dockertest.New() - docker.RunRedis() - log.SetOutput(io.Discard) - code := m.Run() - docker.Purge() - os.Exit(code) - } - code := m.Run() - os.Exit(code) -} diff --git a/infrastructure/cache/cache_test.go b/infrastructure/cache/cache_test.go new file mode 100644 index 0000000..246c45a --- /dev/null +++ b/infrastructure/cache/cache_test.go @@ -0,0 +1,35 @@ +package cache + +import ( + "flag" + "io" + "os" + "testing" + + "github.com/zitryss/aye-and-nay/internal/dockertest" + "github.com/zitryss/aye-and-nay/pkg/log" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestMain(m *testing.M) { + flag.Parse() + if *ci || !*integration { + code := m.Run() + os.Exit(code) + } + log.SetOutput(os.Stderr) + log.SetLevel(log.CRITICAL) + docker := dockertest.New() + host := &DefaultRedisConfig.Host + port := &DefaultRedisConfig.Port + docker.RunRedis(host, port) + log.SetOutput(io.Discard) + code := m.Run() + docker.Purge() + os.Exit(code) +} diff --git a/infrastructure/cache/config.go b/infrastructure/cache/config.go index 5c3e91b..a1efd4a 100644 --- a/infrastructure/cache/config.go +++ b/infrastructure/cache/config.go @@ -2,46 +2,49 @@ package cache import ( "time" - - "github.com/spf13/viper" ) -func newMemConfig() memConfig { - return memConfig{ - limiterRequestsPerSecond: viper.GetFloat64("middleware.limiter.requestsPerSecond"), - limiterBurst: viper.GetInt("middleware.limiter.burst"), - timeToLive: viper.GetDuration("cache.redis.timeToLive"), - cleanupInterval: viper.GetDuration("cache.redis.cleanupInterval"), - } +type CacheConfig struct { + Cache string `mapstructure:"APP_CACHE" validate:"required"` + Mem MemConfig `mapstructure:",squash"` + Redis RedisConfig `mapstructure:",squash"` } -type memConfig struct { - limiterRequestsPerSecond float64 - limiterBurst int - timeToLive time.Duration - cleanupInterval time.Duration +type MemConfig struct { + CleanupInterval time.Duration `mapstructure:"CACHE_MEM_CLEANUP_INTERVAL" validate:"required"` + LimiterRequestsPerSecond float64 `mapstructure:"MIDDLEWARE_LIMITER_REQUESTS_PER_SECOND" validate:"required"` + LimiterBurst int `mapstructure:"MIDDLEWARE_LIMITER_BURST" validate:"required"` + TimeToLive time.Duration `mapstructure:"CACHE_REDIS_TIME_TO_LIVE" validate:"required"` } -func newRedisConfig() redisConfig { - return redisConfig{ - host: viper.GetString("cache.redis.host"), - port: viper.GetString("cache.redis.port"), - times: viper.GetInt("cache.redis.retry.times"), - pause: viper.GetDuration("cache.redis.retry.pause"), - timeout: viper.GetDuration("cache.redis.retry.timeout"), - limiterRequestsPerMinute: viper.GetInt("middleware.limiter.requestsPerSecond"), - limiterBurst: viper.GetInt64("middleware.limiter.burst"), - timeToLive: viper.GetDuration("cache.redis.timeToLive"), - } +type RedisConfig struct { + Host string `mapstructure:"CACHE_REDIS_HOST" validate:"required"` + Port string `mapstructure:"CACHE_REDIS_PORT" validate:"required"` + RetryTimes int `mapstructure:"CACHE_REDIS_RETRY_TIMES" validate:"required"` + RetryPause time.Duration `mapstructure:"CACHE_REDIS_RETRY_PAUSE" validate:"required"` + Timeout time.Duration `mapstructure:"CACHE_REDIS_TIMEOUT" validate:"required"` + LimiterRequestsPerSecond int `mapstructure:"MIDDLEWARE_LIMITER_REQUESTS_PER_SECOND" validate:"required"` + LimiterBurst int64 `mapstructure:"MIDDLEWARE_LIMITER_BURST" validate:"required"` + TimeToLive time.Duration `mapstructure:"CACHE_REDIS_TIME_TO_LIVE" validate:"required"` + TxRetries int `mapstructure:"CACHE_REDIS_TX_RETRIES" validate:"required"` } -type redisConfig struct { - host string - port string - times int - pause time.Duration - timeout time.Duration - limiterRequestsPerMinute int - limiterBurst int64 - timeToLive time.Duration -} +var ( + DefaultMemConfig = MemConfig{ + CleanupInterval: 0, + LimiterRequestsPerSecond: 30000, + LimiterBurst: 300, + TimeToLive: 0, + } + DefaultRedisConfig = RedisConfig{ + Host: "localhost", + Port: "6379", + RetryTimes: 4, + RetryPause: 5 * time.Second, + Timeout: 30 * time.Second, + LimiterRequestsPerSecond: 1, + LimiterBurst: 1, + TimeToLive: 3 * time.Second, + TxRetries: 1, + } +) diff --git a/infrastructure/cache/mem.go b/infrastructure/cache/mem.go index 95ca012..e895d92 100644 --- a/infrastructure/cache/mem.go +++ b/infrastructure/cache/mem.go @@ -13,8 +13,11 @@ import ( "github.com/zitryss/aye-and-nay/pkg/errors" ) -func NewMem(opts ...options) *Mem { - conf := newMemConfig() +var ( + _ domain.Cacher = (*Mem)(nil) +) + +func NewMem(conf MemConfig, opts ...options) *Mem { m := &Mem{ conf: conf, syncVisitors: syncVisitors{visitors: map[uint64]*visitorTime{}}, @@ -31,28 +34,35 @@ func NewMem(opts ...options) *Mem { type options func(*Mem) -func WithHeartbeatPair(ch chan<- interface{}) options { +func WithHeartbeatCleanup(ch chan<- any) options { + return func(m *Mem) { + m.heartbeat.cleanup = ch + } +} + +func WithHeartbeatPair(ch chan<- any) options { return func(m *Mem) { m.heartbeat.pair = ch } } -func WithHeartbeatToken(ch chan<- interface{}) options { +func WithHeartbeatToken(ch chan<- any) options { return func(m *Mem) { m.heartbeat.token = ch } } type Mem struct { - conf memConfig + conf MemConfig syncVisitors syncQueues syncPQueues syncPairs syncTokens heartbeat struct { - pair chan<- interface{} - token chan<- interface{} + cleanup chan<- any + pair chan<- any + token chan<- any } } @@ -92,7 +102,8 @@ type syncTokens struct { } type tokenTime struct { - token uint64 + album uint64 + image uint64 seen time.Time } @@ -101,7 +112,7 @@ type elem struct { expires time.Time } -func timeComparator(a, b interface{}) int { +func timeComparator(a, b any) int { tA := a.(elem).expires tB := b.(elem).expires switch { @@ -114,55 +125,100 @@ func timeComparator(a, b interface{}) int { } } -func (m *Mem) Monitor() { +func (m *Mem) Monitor(ctx context.Context) { go func() { for { + select { + case <-ctx.Done(): + return + default: + } + if m.heartbeat.cleanup != nil { + select { + case <-ctx.Done(): + return + case m.heartbeat.cleanup <- struct{}{}: + } + } now := time.Now() m.syncVisitors.Lock() for k, v := range m.visitors { - if now.Sub(v.seen) >= m.conf.timeToLive { + if now.Sub(v.seen) >= m.conf.TimeToLive { delete(m.visitors, k) } } m.syncVisitors.Unlock() - time.Sleep(m.conf.cleanupInterval) + time.Sleep(m.conf.CleanupInterval) + if m.heartbeat.cleanup != nil { + select { + case <-ctx.Done(): + return + case m.heartbeat.cleanup <- struct{}{}: + } + } } }() go func() { for { + select { + case <-ctx.Done(): + return + default: + } if m.heartbeat.pair != nil { - m.heartbeat.pair <- struct{}{} + select { + case <-ctx.Done(): + return + case m.heartbeat.pair <- struct{}{}: + } } now := time.Now() m.syncPairs.Lock() for k, v := range m.pairs { - if now.Sub(v.seen) >= m.conf.timeToLive { + if now.Sub(v.seen) >= m.conf.TimeToLive { delete(m.pairs, k) } } m.syncPairs.Unlock() - time.Sleep(m.conf.cleanupInterval) + time.Sleep(m.conf.CleanupInterval) if m.heartbeat.pair != nil { - m.heartbeat.pair <- struct{}{} + select { + case <-ctx.Done(): + return + case m.heartbeat.pair <- struct{}{}: + } } } }() go func() { for { + select { + case <-ctx.Done(): + return + default: + } if m.heartbeat.token != nil { - m.heartbeat.token <- struct{}{} + select { + case <-ctx.Done(): + return + case m.heartbeat.token <- struct{}{}: + } } now := time.Now() m.syncTokens.Lock() for k, v := range m.tokens { - if now.Sub(v.seen) >= m.conf.timeToLive { + if now.Sub(v.seen) >= m.conf.TimeToLive { delete(m.tokens, k) } } m.syncTokens.Unlock() - time.Sleep(m.conf.cleanupInterval) + time.Sleep(m.conf.CleanupInterval) if m.heartbeat.token != nil { - m.heartbeat.token <- struct{}{} + select { + case <-ctx.Done(): + return + case m.heartbeat.token <- struct{}{}: + } } } }() @@ -173,7 +229,7 @@ func (m *Mem) Allow(_ context.Context, ip uint64) (bool, error) { defer m.syncVisitors.Unlock() v, ok := m.visitors[ip] if !ok { - l := rate.NewLimiter(rate.Limit(m.conf.limiterRequestsPerSecond), m.conf.limiterBurst) + l := rate.NewLimiter(rate.Limit(m.conf.LimiterRequestsPerSecond), m.conf.LimiterBurst) v = &visitorTime{limiter: l} m.visitors[ip] = v } @@ -289,7 +345,7 @@ func (m *Mem) Pop(_ context.Context, album uint64) (uint64, uint64, error) { return images[0], images[1], nil } -func (m *Mem) Set(_ context.Context, _ uint64, token uint64, image uint64) error { +func (m *Mem) Set(_ context.Context, token uint64, album uint64, image uint64) error { m.syncTokens.Lock() defer m.syncTokens.Unlock() _, ok := m.tokens[token] @@ -297,19 +353,49 @@ func (m *Mem) Set(_ context.Context, _ uint64, token uint64, image uint64) error return errors.Wrap(domain.ErrTokenAlreadyExists) } t := &tokenTime{} - t.token = image + t.album = album + t.image = image t.seen = time.Now() m.tokens[token] = t return nil } -func (m *Mem) Get(_ context.Context, _ uint64, token uint64) (uint64, error) { +func (m *Mem) Get(_ context.Context, token uint64) (uint64, uint64, error) { m.syncTokens.Lock() defer m.syncTokens.Unlock() - image, ok := m.tokens[token] + t, ok := m.tokens[token] if !ok { - return 0x0, errors.Wrap(domain.ErrTokenNotFound) + return 0x0, 0x0, errors.Wrap(domain.ErrTokenNotFound) } + return t.album, t.image, nil +} + +func (m *Mem) Del(_ context.Context, token uint64) error { + m.syncTokens.Lock() + defer m.syncTokens.Unlock() delete(m.tokens, token) - return image.token, nil + return nil +} + +func (m *Mem) Health(_ context.Context) (bool, error) { + return true, nil +} + +func (m *Mem) Reset() error { + m.syncVisitors.Lock() + defer m.syncVisitors.Unlock() + m.visitors = map[uint64]*visitorTime{} + m.syncQueues.Lock() + defer m.syncQueues.Unlock() + m.queues = map[uint64]*linkedhashset.Set{} + m.syncPQueues.Lock() + defer m.syncPQueues.Unlock() + m.pqueues = map[uint64]*binaryheap.Heap{} + m.syncPairs.Lock() + defer m.syncPairs.Unlock() + m.pairs = map[uint64]*pairsTime{} + m.syncTokens.Lock() + defer m.syncTokens.Unlock() + m.tokens = map[uint64]*tokenTime{} + return nil } diff --git a/infrastructure/cache/mem_test.go b/infrastructure/cache/mem_test.go index 6139247..bd00d84 100644 --- a/infrastructure/cache/mem_test.go +++ b/infrastructure/cache/mem_test.go @@ -1,5 +1,3 @@ -//go:build unit - package cache import ( @@ -7,132 +5,190 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/zitryss/aye-and-nay/domain/domain" - _ "github.com/zitryss/aye-and-nay/internal/config" + . "github.com/zitryss/aye-and-nay/internal/generator" . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" ) -func TestMemPair(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - err := mem.Push(context.Background(), 0x23D2, [][2]uint64{{0x3E3D, 0xB399}}) - if err != nil { - t.Error(err) - } - image1, image2, err := mem.Pop(context.Background(), 0x23D2) - if err != nil { - t.Error(err) - } - if image1 != 0x3E3D { - t.Error("image1 != 0x3E3D") - } - if image2 != 0xB399 { - t.Error("image2 != 0xB399") - } +func TestMemTestSuite(t *testing.T) { + suite.Run(t, &MemTestSuite{}) +} + +type MemTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + conf MemConfig + heartbeatCleanup chan any + heartbeatPair chan any + heartbeatToken chan any + cache domain.Cacher + setupTestFn func() +} + +func (suite *MemTestSuite) SetupSuite() { + if !*unit { + suite.T().Skip() + } + ctx, cancel := context.WithCancel(context.Background()) + conf := DefaultMemConfig + hc := make(chan any) + hp := make(chan any) + ht := make(chan any) + mem := NewMem(conf, WithHeartbeatCleanup(hc), WithHeartbeatPair(hp), WithHeartbeatToken(ht)) + mem.Monitor(ctx) + suite.ctx = ctx + suite.cancel = cancel + suite.conf = conf + suite.heartbeatCleanup = hc + suite.heartbeatPair = hp + suite.heartbeatToken = ht + suite.cache = mem + suite.setupTestFn = suite.SetupTest +} + +func (suite *MemTestSuite) SetupTest() { + err := suite.cache.(*Mem).Reset() + require.NoError(suite.T(), err) +} + +func (suite *MemTestSuite) TearDownTest() { + +} + +func (suite *MemTestSuite) TearDownSuite() { + err := suite.cache.(*Mem).Reset() + require.NoError(suite.T(), err) + suite.cancel() +} + +func (suite *MemTestSuite) TestPair() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + album := id() + pairs := [][2]uint64{{id(), id()}} + err := suite.cache.Push(suite.ctx, album, pairs) + assert.NoError(t, err) + image1, image2, err := suite.cache.Pop(suite.ctx, album) + assert.NoError(t, err) + assert.Equal(t, ids.Uint64(1), image1) + assert.Equal(t, ids.Uint64(2), image2) }) - t.Run("Negative1", func(t *testing.T) { - mem := NewMem() - _, _, err := mem.Pop(context.Background(), 0x73BF) - if !errors.Is(err, domain.ErrPairNotFound) { - t.Error(err) - } + suite.T().Run("Negative1", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + album := id() + _, _, err := suite.cache.Pop(suite.ctx, album) + assert.ErrorIs(t, err, domain.ErrPairNotFound) }) - t.Run("Negative2", func(t *testing.T) { - mem := NewMem() - err := mem.Push(context.Background(), 0x1AE9, [][2]uint64{{0x44DC, 0x721B}}) - if err != nil { - t.Error(err) - } - _, _, err = mem.Pop(context.Background(), 0x1AE9) - if err != nil { - t.Error(err) - } - _, _, err = mem.Pop(context.Background(), 0x1AE9) - if !errors.Is(err, domain.ErrPairNotFound) { - t.Error(err) - } + suite.T().Run("Negative2", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + album := id() + pairs := [][2]uint64{{id(), id()}} + err := suite.cache.Push(suite.ctx, album, pairs) + assert.NoError(t, err) + _, _, err = suite.cache.Pop(suite.ctx, album) + assert.NoError(t, err) + _, _, err = suite.cache.Pop(suite.ctx, album) + assert.ErrorIs(t, err, domain.ErrPairNotFound) }) - t.Run("Negative3", func(t *testing.T) { - heartbeatPair := make(chan interface{}) - mem := NewMem(WithHeartbeatPair(heartbeatPair)) - mem.Monitor() - err := mem.Push(context.Background(), 0xF51A, [][2]uint64{{0x4BB0, 0x3A87}}) - if err != nil { - t.Error(err) - } - time.Sleep(mem.conf.timeToLive) - CheckChannel(t, heartbeatPair) - CheckChannel(t, heartbeatPair) - _, _, err = mem.Pop(context.Background(), 0xF51A) - if !errors.Is(err, domain.ErrPairNotFound) { - t.Error(err) - } + suite.T().Run("Negative3", func(t *testing.T) { + suite.setupTestFn() + _, ok := suite.cache.(*Redis) + if testing.Short() && ok { + t.Skip("short flag is set") + } + id, _ := GenId() + album := id() + pairs := [][2]uint64{{id(), id()}} + err := suite.cache.Push(suite.ctx, album, pairs) + assert.NoError(t, err) + time.Sleep(suite.conf.TimeToLive * 2) + AssertChannel(t, suite.heartbeatPair) + AssertChannel(t, suite.heartbeatPair) + _, _, err = suite.cache.Pop(suite.ctx, album) + assert.ErrorIs(t, err, domain.ErrPairNotFound) }) } -func TestMemToken(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - err := mem.Set(context.Background(), 0xC2E7, 0xB41C, 0x52BD) - if err != nil { - t.Error(err) - } - image, err := mem.Get(context.Background(), 0xC2E7, 0xB41C) - if err != nil { - t.Error(err) - } - if image != 0x52BD { - t.Error("image != 0x52BD") - } +func (suite *MemTestSuite) TestToken() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + token := id() + album1 := id() + image1 := id() + err := suite.cache.Set(suite.ctx, token, album1, image1) + assert.NoError(t, err) + album2, image2, err := suite.cache.Get(suite.ctx, token) + assert.NoError(t, err) + assert.Equal(t, album1, album2) + assert.Equal(t, image1, image2) }) - t.Run("Negative1", func(t *testing.T) { - mem := NewMem() - err := mem.Set(context.Background(), 0x1C4A, 0xF0EE, 0x583C) - if err != nil { - t.Error(err) - } - err = mem.Set(context.Background(), 0x1C4A, 0xF0EE, 0x583C) - if !errors.Is(err, domain.ErrTokenAlreadyExists) { - t.Error(err) - } + suite.T().Run("Negative1", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + token := id() + album := id() + image := id() + err := suite.cache.Set(suite.ctx, token, album, image) + assert.NoError(t, err) + err = suite.cache.Set(suite.ctx, token, album, image) + assert.ErrorIs(t, err, domain.ErrTokenAlreadyExists) }) - t.Run("Negative2", func(t *testing.T) { - mem := NewMem() - _, err := mem.Get(context.Background(), 0x1C4A, 0xC4F8) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } + suite.T().Run("Negative2", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + token := id() + _, _, err := suite.cache.Get(suite.ctx, token) + assert.ErrorIs(t, err, domain.ErrTokenNotFound) }) - t.Run("Negative3", func(t *testing.T) { - mem := NewMem() - err := mem.Set(context.Background(), 0xEB96, 0xC67F, 0x7C45) - if err != nil { - t.Error(err) - } - _, err = mem.Get(context.Background(), 0xEB96, 0xC67F) - if err != nil { - t.Error(err) - } - _, err = mem.Get(context.Background(), 0xEB96, 0xC67F) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } + suite.T().Run("Negative3", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + token := id() + album := id() + image := id() + err := suite.cache.Set(suite.ctx, token, album, image) + assert.NoError(t, err) + _, _, err = suite.cache.Get(suite.ctx, token) + assert.NoError(t, err) + err = suite.cache.Del(suite.ctx, token) + assert.NoError(t, err) + err = suite.cache.Del(suite.ctx, token) + assert.NoError(t, err) + _, _, err = suite.cache.Get(suite.ctx, token) + assert.ErrorIs(t, err, domain.ErrTokenNotFound) }) - t.Run("Negative4", func(t *testing.T) { - heartbeatToken := make(chan interface{}) - mem := NewMem(WithHeartbeatToken(heartbeatToken)) - mem.Monitor() - err := mem.Set(context.Background(), 0xE0AF, 0xCF1E, 0xDD0A) - if err != nil { - t.Error(err) - } - time.Sleep(mem.conf.timeToLive) - CheckChannel(t, heartbeatToken) - CheckChannel(t, heartbeatToken) - _, err = mem.Get(context.Background(), 0xE0AF, 0xCF1E) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } + suite.T().Run("Negative4", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + token := id() + err := suite.cache.Del(suite.ctx, token) + assert.NoError(t, err) + }) + suite.T().Run("Negative5", func(t *testing.T) { + suite.setupTestFn() + _, ok := suite.cache.(*Redis) + if testing.Short() && ok { + t.Skip("short flag is set") + } + id, _ := GenId() + token := id() + album := id() + image := id() + err := suite.cache.Set(suite.ctx, token, album, image) + assert.NoError(t, err) + time.Sleep(suite.conf.TimeToLive * 2) + AssertChannel(t, suite.heartbeatToken) + AssertChannel(t, suite.heartbeatToken) + _, _, err = suite.cache.Get(suite.ctx, token) + assert.ErrorIs(t, err, domain.ErrTokenNotFound) }) } diff --git a/infrastructure/cache/redis.go b/infrastructure/cache/redis.go index ff789fe..995424e 100644 --- a/infrastructure/cache/redis.go +++ b/infrastructure/cache/redis.go @@ -2,11 +2,11 @@ package cache import ( "context" - "strconv" "strings" "time" redisdb "github.com/go-redis/redis/v8" + "github.com/go-redis/redis_rate/v9" "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/pkg/base64" @@ -14,13 +14,19 @@ import ( "github.com/zitryss/aye-and-nay/pkg/retry" ) -func NewRedis() (*Redis, error) { - conf := newRedisConfig() - client := redisdb.NewClient(&redisdb.Options{Addr: conf.host + ":" + conf.port}) - ctx, cancel := context.WithTimeout(context.Background(), conf.timeout) +var ( + _ domain.Cacher = (*Redis)(nil) +) + +func NewRedis(ctx context.Context, conf RedisConfig) (*Redis, error) { + client := redisdb.NewClient(&redisdb.Options{Addr: conf.Host + ":" + conf.Port}) + r := &Redis{} + r.conf = conf + r.client = client + ctx, cancel := context.WithTimeout(ctx, conf.Timeout) defer cancel() - err := retry.Do(conf.times, conf.pause, func() error { - err := client.Ping(ctx).Err() + err := retry.Do(conf.RetryTimes, conf.RetryPause, func() error { + _, err := r.Health(ctx) if err != nil { return errors.Wrap(err) } @@ -29,58 +35,59 @@ func NewRedis() (*Redis, error) { if err != nil { return &Redis{}, errors.Wrap(err) } - return &Redis{conf, client}, nil + r.limiter = redis_rate.NewLimiter(client) + r.limit = redis_rate.PerSecond(conf.LimiterRequestsPerSecond) + return r, nil } type Redis struct { - conf redisConfig - client *redisdb.Client + conf RedisConfig + client *redisdb.Client + limiter *redis_rate.Limiter + limit redis_rate.Limit } func (r *Redis) Allow(ctx context.Context, ip uint64) (bool, error) { ipB64 := base64.FromUint64(ip) key := "ip:" + ipB64 - value, err := r.client.Get(ctx, key).Result() - if err != nil && !errors.Is(err, redisdb.Nil) { - return false, errors.Wrap(err) - } - if errors.Is(err, redisdb.Nil) { - value = "-1" - } - count, err := strconv.Atoi(value) + res, err := r.limiter.Allow(ctx, key, r.limit) if err != nil { return false, errors.Wrap(err) } - if count >= r.conf.limiterRequestsPerMinute { - return false, nil - } - pipe := r.client.Pipeline() - pipe.IncrBy(ctx, key, r.conf.limiterBurst) - pipe.Expire(ctx, key, 59*time.Second) - _, err = pipe.Exec(ctx) - if err != nil { - return false, errors.Wrap(err) - } - return true, nil + return res.Allowed > 0, nil } func (r *Redis) Add(ctx context.Context, queue uint64, album uint64) error { queueB64 := base64.FromUint64(queue) - albumB64 := base64.FromUint64(album) key1 := "queue:" + queueB64 + ":set" - ok, err := r.client.SIsMember(ctx, key1, albumB64).Result() - if err != nil { - return errors.Wrap(err) - } - if ok { + key2 := "queue:" + queueB64 + ":list" + txFn := func(tx *redisdb.Tx) error { + albumB64 := base64.FromUint64(album) + ok, err := tx.SIsMember(ctx, key1, albumB64).Result() + if err != nil { + return errors.Wrap(err) + } + if ok { + return nil + } + _, err = tx.SAdd(ctx, key1, albumB64).Result() + if err != nil { + return errors.Wrap(err) + } + _, err = tx.RPush(ctx, key2, albumB64).Result() + if err != nil { + return errors.Wrap(err) + } return nil } - _, err = r.client.SAdd(ctx, key1, albumB64).Result() - if err != nil { - return errors.Wrap(err) + err := error(nil) + for i := 0; i < r.conf.TxRetries; i++ { + err = r.client.Watch(ctx, txFn, key1, key2) + if err != nil { + continue + } + break } - key2 := "queue:" + queueB64 + ":list" - _, err = r.client.RPush(ctx, key2, albumB64).Result() if err != nil { return errors.Wrap(err) } @@ -90,16 +97,31 @@ func (r *Redis) Add(ctx context.Context, queue uint64, album uint64) error { func (r *Redis) Poll(ctx context.Context, queue uint64) (uint64, error) { queueB64 := base64.FromUint64(queue) key1 := "queue:" + queueB64 + ":list" - albumB64, err := r.client.LPop(ctx, key1).Result() - if errors.Is(err, redisdb.Nil) { - return 0x0, errors.Wrap(domain.ErrUnknown) - } key2 := "queue:" + queueB64 + ":set" - _, err = r.client.SRem(ctx, key2, albumB64).Result() - if err != nil { - return 0x0, errors.Wrap(err) + album := uint64(0x0) + txFn := func(tx *redisdb.Tx) error { + albumB64, err := tx.LPop(ctx, key1).Result() + if errors.Is(err, redisdb.Nil) { + return errors.Wrap(domain.ErrUnknown) + } + _, err = tx.SRem(ctx, key2, albumB64).Result() + if err != nil { + return errors.Wrap(err) + } + album, err = base64.ToUint64(albumB64) + if err != nil { + return errors.Wrap(err) + } + return nil + } + err := error(nil) + for i := 0; i < r.conf.TxRetries; i++ { + err = r.client.Watch(ctx, txFn, key1, key2) + if err != nil { + continue + } + break } - album, err := base64.ToUint64(albumB64) if err != nil { return 0x0, errors.Wrap(err) } @@ -108,12 +130,36 @@ func (r *Redis) Poll(ctx context.Context, queue uint64) (uint64, error) { func (r *Redis) Size(ctx context.Context, queue uint64) (int, error) { queueB64 := base64.FromUint64(queue) - key := "queue:" + queueB64 + ":set" - n, err := r.client.SCard(ctx, key).Result() + key1 := "queue:" + queueB64 + ":set" + key2 := "queue:" + queueB64 + ":list" + n := 0 + txFn := func(tx *redisdb.Tx) error { + n1, err := r.client.SCard(ctx, key1).Result() + if err != nil { + return errors.Wrap(err) + } + n2, err := r.client.LLen(ctx, key2).Result() + if err != nil { + return errors.Wrap(err) + } + if n1 != n2 { + return errors.Wrap(domain.ErrUnknown) + } + n = int(n1) + return nil + } + err := error(nil) + for i := 0; i < r.conf.TxRetries; i++ { + err = r.client.Watch(ctx, txFn, key1, key2) + if err != nil { + continue + } + break + } if err != nil { return 0, errors.Wrap(err) } - return int(n), nil + return n, nil } func (r *Redis) PAdd(ctx context.Context, pqueue uint64, album uint64, expires time.Time) error { @@ -156,15 +202,15 @@ func (r *Redis) PSize(ctx context.Context, pqueue uint64) (int, error) { } func (r *Redis) Push(ctx context.Context, album uint64, pairs [][2]uint64) error { + pipe := r.client.Pipeline() albumB64 := base64.FromUint64(album) key := "album:" + albumB64 + ":pairs" - pipe := r.client.Pipeline() for _, images := range pairs { image0B64 := base64.FromUint64(images[0]) image1B64 := base64.FromUint64(images[1]) pipe.RPush(ctx, key, image0B64+":"+image1B64) } - pipe.Expire(ctx, key, r.conf.timeToLive) + pipe.Expire(ctx, key, r.conf.TimeToLive) _, err := pipe.Exec(ctx) if err != nil { return errors.Wrap(err) @@ -175,38 +221,34 @@ func (r *Redis) Push(ctx context.Context, album uint64, pairs [][2]uint64) error func (r *Redis) Pop(ctx context.Context, album uint64) (uint64, uint64, error) { albumB64 := base64.FromUint64(album) key := "album:" + albumB64 + ":pairs" - n, err := r.client.LLen(ctx, key).Result() - if err != nil { - return 0x0, 0x0, errors.Wrap(err) - } - if n == 0 { + val, err := r.client.LPop(ctx, key).Result() + if errors.Is(err, redisdb.Nil) { return 0x0, 0x0, errors.Wrap(domain.ErrPairNotFound) } - val, err := r.client.LPop(ctx, key).Result() if err != nil { return 0x0, 0x0, errors.Wrap(err) } - _ = r.client.Expire(ctx, key, r.conf.timeToLive) - imagesB64 := strings.Split(val, ":") - if len(imagesB64) != 2 { + _ = r.client.Expire(ctx, key, r.conf.TimeToLive) + image1B64, image2B64, found := strings.Cut(val, ":") + if !found { return 0x0, 0x0, errors.Wrap(domain.ErrUnknown) } - image0, err := base64.ToUint64(imagesB64[0]) + image0, err := base64.ToUint64(image1B64) if err != nil { return 0x0, 0x0, errors.Wrap(err) } - image1, err := base64.ToUint64(imagesB64[1]) + image1, err := base64.ToUint64(image2B64) if err != nil { return 0x0, 0x0, errors.Wrap(err) } return image0, image1, nil } -func (r *Redis) Set(ctx context.Context, album uint64, token uint64, image uint64) error { - albumB64 := base64.FromUint64(album) +func (r *Redis) Set(ctx context.Context, token uint64, album uint64, image uint64) error { tokenB64 := base64.FromUint64(token) + albumB64 := base64.FromUint64(album) imageB64 := base64.FromUint64(image) - key := "album:" + albumB64 + ":token:" + tokenB64 + ":image" + key := "token:" + tokenB64 n, err := r.client.Exists(ctx, key).Result() if err != nil { return errors.Wrap(err) @@ -214,39 +256,68 @@ func (r *Redis) Set(ctx context.Context, album uint64, token uint64, image uint6 if n == 1 { return errors.Wrap(domain.ErrTokenAlreadyExists) } - err = r.client.Set(ctx, key, imageB64, r.conf.timeToLive).Err() + err = r.client.Set(ctx, key, albumB64+":"+imageB64, r.conf.TimeToLive).Err() if err != nil { return errors.Wrap(err) } return nil } -func (r *Redis) Get(ctx context.Context, album uint64, token uint64) (uint64, error) { - albumB64 := base64.FromUint64(album) +func (r *Redis) Get(ctx context.Context, token uint64) (uint64, uint64, error) { tokenB64 := base64.FromUint64(token) - key := "album:" + albumB64 + ":token:" + tokenB64 + ":image" - imageB64, err := r.client.Get(ctx, key).Result() + key := "token:" + tokenB64 + s, err := r.client.Get(ctx, key).Result() if errors.Is(err, redisdb.Nil) { - return 0x0, errors.Wrap(domain.ErrTokenNotFound) + return 0x0, 0x0, errors.Wrap(domain.ErrTokenNotFound) } if err != nil { - return 0x0, errors.Wrap(err) + return 0x0, 0x0, errors.Wrap(err) } - err = r.client.Del(ctx, key).Err() + albumB64, imageB64, found := strings.Cut(s, ":") + if !found { + return 0x0, 0x0, errors.Wrap(domain.ErrUnknown) + } + album, err := base64.ToUint64(albumB64) if err != nil { - return 0x0, errors.Wrap(err) + return 0x0, 0x0, errors.Wrap(err) } image, err := base64.ToUint64(imageB64) if err != nil { - return 0x0, errors.Wrap(err) + return 0x0, 0x0, errors.Wrap(err) } - return image, nil + return album, image, nil } -func (r *Redis) Close() error { +func (r *Redis) Del(ctx context.Context, token uint64) error { + tokenB64 := base64.FromUint64(token) + key := "token:" + tokenB64 + err := r.client.Del(ctx, key).Err() + if err != nil { + return errors.Wrap(err) + } + return nil +} + +func (r *Redis) Health(ctx context.Context) (bool, error) { + err := r.client.Ping(ctx).Err() + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthCache, "%s", err) + } + return true, nil +} + +func (r *Redis) Close(_ context.Context) error { err := r.client.Close() if err != nil { return errors.Wrap(err) } return nil } + +func (r *Redis) Reset() error { + err := r.client.FlushAll(context.Background()).Err() + if err != nil { + return errors.Wrap(err) + } + return nil +} diff --git a/infrastructure/cache/redis_integration_test.go b/infrastructure/cache/redis_integration_test.go index ca5b6e6..74a2fa4 100644 --- a/infrastructure/cache/redis_integration_test.go +++ b/infrastructure/cache/redis_integration_test.go @@ -1,5 +1,3 @@ -//go:build integration - package cache import ( @@ -7,360 +5,185 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/zitryss/aye-and-nay/domain/domain" - _ "github.com/zitryss/aye-and-nay/internal/config" - "github.com/zitryss/aye-and-nay/pkg/errors" + . "github.com/zitryss/aye-and-nay/internal/generator" ) -func TestRedisAllow(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - if testing.Short() { - t.Skip("short flag is set") - } - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - rpm := redis.conf.limiterRequestsPerMinute - for j := 0; j < rpm; j++ { - allowed, err := redis.Allow(context.Background(), 0xDEAD) - if err != nil { - t.Error(err) - } - if !allowed { - t.Error("!allowed") - } - } - time.Sleep(60 * time.Second) - for j := 0; j < rpm; j++ { - allowed, err := redis.Allow(context.Background(), 0xDEAD) - if err != nil { - t.Error(err) - } - if !allowed { - t.Error("!allowed") - } - } - }) - t.Run("Negative", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - rps := redis.conf.limiterRequestsPerMinute - for i := 0; i < rps; i++ { - allowed, err := redis.Allow(context.Background(), 0xBEEF) - if err != nil { - t.Error(err) - } - if !allowed { - t.Error("!allowed") - } - } - allowed, err := redis.Allow(context.Background(), 0xBEEF) - if err != nil { - t.Error(err) - } - if allowed { - t.Error("allowed") - } - }) +func TestRedisTestSuite(t *testing.T) { + suite.Run(t, &RedisTestSuite{}) } -func TestRedisQueue(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - n, err := redis.Size(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - err = redis.Add(context.Background(), 0x5D6D, 0x1ED1) - if err != nil { - t.Error(err) - } - err = redis.Add(context.Background(), 0x5D6D, 0x1ED1) - if err != nil { - t.Error(err) - } - err = redis.Add(context.Background(), 0x5D6D, 0xF612) - if err != nil { - t.Error(err) - } - err = redis.Add(context.Background(), 0x5D6D, 0x1A83) - if err != nil { - t.Error(err) - } - err = redis.Add(context.Background(), 0x5D6D, 0xF612) - if err != nil { - t.Error(err) - } - n, err = redis.Size(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if n != 3 { - t.Error("n != 3") - } - album, err := redis.Poll(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if album != 0x1ED1 { - t.Error("album != 0x1ED1") - } - n, err = redis.Size(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if n != 2 { - t.Error("n != 2") - } - album, err = redis.Poll(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if album != 0xF612 { - t.Error("album != 0xF612") - } - album, err = redis.Poll(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if album != 0x1A83 { - t.Error("album != 0x1A83") - } - n, err = redis.Size(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - album, err = redis.Poll(context.Background(), 0x5D6D) - if err == nil { - t.Error(err) - } - if album != 0x0 { - t.Error("album != \"0x0\"") - } - n, err = redis.Size(context.Background(), 0x5D6D) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - _, err = redis.Poll(context.Background(), 0x5D6D) - if !errors.Is(err, domain.ErrUnknown) { - t.Error(err) - } +type RedisTestSuite struct { + suite.Suite + base MemTestSuite + setupTestFn func() } -func TestRedisPQueue(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - n, err := redis.PSize(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - err = redis.PAdd(context.Background(), 0x7D31, 0xE976, time.Unix(904867200, 0)) - if err != nil { - t.Error(err) - } - err = redis.PAdd(context.Background(), 0x7D31, 0xEC0E, time.Unix(1075852800, 0)) - if err != nil { - t.Error(err) - } - err = redis.PAdd(context.Background(), 0x7D31, 0x4CAF, time.Unix(681436800, 0)) - if err != nil { - t.Error(err) - } - n, err = redis.PSize(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if n != 3 { - t.Error("n != 3") - } - album, expires, err := redis.PPoll(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if album != 0x4CAF { - t.Error("album != 0x4CAF") - } - if !expires.Equal(time.Unix(681436800, 0)) { - t.Error("!expires.Equal(time.Unix(681436800, 0))") - } - n, err = redis.PSize(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if n != 2 { - t.Error("n != 2") - } - album, expires, err = redis.PPoll(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if album != 0xE976 { - t.Error("album != 0xE976") - } - if !expires.Equal(time.Unix(904867200, 0)) { - t.Error("!expires.Equal(time.Unix(904867200, 0))") - } - album, expires, err = redis.PPoll(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if album != 0xEC0E { - t.Error("album != 0xEC0E") - } - if !expires.Equal(time.Unix(1075852800, 0)) { - t.Error("!expires.Equal(time.Unix(1075852800, 0))") - } - n, err = redis.PSize(context.Background(), 0x7D31) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - _, _, err = redis.PPoll(context.Background(), 0x7D31) - if !errors.Is(err, domain.ErrUnknown) { - t.Error(err) - } +func (suite *RedisTestSuite) SetupSuite() { + if !*integration { + suite.T().Skip() + } + suite.base = MemTestSuite{} + suite.base.SetT(suite.T()) + ctx, cancel := context.WithCancel(context.Background()) + conf := DefaultRedisConfig + redis, err := NewRedis(ctx, conf) + require.NoError(suite.T(), err) + suite.base.ctx = ctx + suite.base.cancel = cancel + suite.base.conf.LimiterRequestsPerSecond = float64(conf.LimiterRequestsPerSecond) + suite.base.conf.TimeToLive = conf.TimeToLive + suite.base.cache = redis + suite.base.setupTestFn = suite.SetupTest + suite.setupTestFn = suite.SetupTest } -func TestRedisPair(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - image1 := uint64(0x3E3D) - image2 := uint64(0xB399) - err = redis.Push(context.Background(), 0x23D2, [][2]uint64{{image1, image2}}) - if err != nil { - t.Error(err) - } - image3, image4, err := redis.Pop(context.Background(), 0x23D2) - if err != nil { - t.Error(err) - } - if image1 != image3 { - t.Error("image1 != image3") - } - if image2 != image4 { - t.Error("image2 != image4") - } - }) - t.Run("Negative1", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - _, _, err = redis.Pop(context.Background(), 0x73BF) - if !errors.Is(err, domain.ErrPairNotFound) { - t.Error(err) - } - }) - t.Run("Negative2", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - image1 := uint64(0x44DC) - image2 := uint64(0x721B) - err = redis.Push(context.Background(), 0x1AE9, [][2]uint64{{image1, image2}}) - if err != nil { - t.Error(err) - } - _, _, err = redis.Pop(context.Background(), 0x1AE9) - if err != nil { - t.Error(err) - } - _, _, err = redis.Pop(context.Background(), 0x1AE9) - if !errors.Is(err, domain.ErrPairNotFound) { - t.Error(err) - } - }) +func (suite *RedisTestSuite) SetupTest() { + err := suite.base.cache.(*Redis).Reset() + require.NoError(suite.T(), err) } -func TestRedisToken(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - image1 := uint64(0x52BD) - token := uint64(0xB41C) - err = redis.Set(context.Background(), 0xC2E7, token, image1) - if err != nil { - t.Error(err) - } - image2, err := redis.Get(context.Background(), 0xC2E7, token) - if err != nil { - t.Error(err) - } - if image1 != image2 { - t.Error("image1 != image2") - } - }) - t.Run("Negative1", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - image := uint64(0x583C) - token := uint64(0xF0EE) - err = redis.Set(context.Background(), 0x1C4A, token, image) - if err != nil { - t.Error(err) - } - err = redis.Set(context.Background(), 0x1C4A, token, image) - if !errors.Is(err, domain.ErrTokenAlreadyExists) { - t.Error(err) +func (suite *RedisTestSuite) TearDownTest() { + +} + +func (suite *RedisTestSuite) TearDownSuite() { + err := suite.base.cache.(*Redis).Reset() + require.NoError(suite.T(), err) + err = suite.base.cache.(*Redis).Close(suite.base.ctx) + require.NoError(suite.T(), err) + suite.base.cancel() +} + +func (suite *RedisTestSuite) TestRedisAllow() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + rpm := suite.base.conf.LimiterRequestsPerSecond + ip := id() + for j := float64(0); j < rpm; j++ { + allowed, err := suite.base.cache.Allow(suite.base.ctx, ip) + assert.NoError(t, err) + assert.True(t, allowed) + } + time.Sleep(1 * time.Second) + for j := float64(0); j < rpm; j++ { + allowed, err := suite.base.cache.Allow(suite.base.ctx, ip) + assert.NoError(t, err) + assert.True(t, allowed) } }) - t.Run("Negative2", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - _, err = redis.Get(context.Background(), 0x1C4A, 0xC4F8) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } - }) - t.Run("Negative3", func(t *testing.T) { - redis, err := NewRedis() - if err != nil { - t.Fatal(err) - } - image := uint64(0x7C45) - token := uint64(0xC67F) - err = redis.Set(context.Background(), 0xEB96, token, image) - if err != nil { - t.Error(err) - } - _, err = redis.Get(context.Background(), 0xEB96, token) - if err != nil { - t.Error(err) - } - _, err = redis.Get(context.Background(), 0xEB96, token) - if !errors.Is(err, domain.ErrTokenNotFound) { - t.Error(err) - } + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + rps := suite.base.conf.LimiterRequestsPerSecond + ip := id() + for i := float64(0); i < rps; i++ { + allowed, err := suite.base.cache.Allow(suite.base.ctx, ip) + assert.NoError(t, err) + assert.True(t, allowed) + } + allowed, err := suite.base.cache.Allow(suite.base.ctx, ip) + assert.NoError(t, err) + assert.False(t, allowed) }) } + +func (suite *RedisTestSuite) TestRedisQueue() { + id, _ := GenId() + queue := id() + albumExp1 := id() + albumExp2 := id() + albumExp3 := id() + n, err := suite.base.cache.Size(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, n) + err = suite.base.cache.Add(suite.base.ctx, queue, albumExp1) + assert.NoError(suite.T(), err) + err = suite.base.cache.Add(suite.base.ctx, queue, albumExp1) + assert.NoError(suite.T(), err) + err = suite.base.cache.Add(suite.base.ctx, queue, albumExp2) + assert.NoError(suite.T(), err) + err = suite.base.cache.Add(suite.base.ctx, queue, albumExp3) + assert.NoError(suite.T(), err) + err = suite.base.cache.Add(suite.base.ctx, queue, albumExp2) + assert.NoError(suite.T(), err) + n, err = suite.base.cache.Size(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 3, n) + album, err := suite.base.cache.Poll(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), albumExp1, album) + n, err = suite.base.cache.Size(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 2, n) + album, err = suite.base.cache.Poll(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), albumExp2, album) + album, err = suite.base.cache.Poll(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), albumExp3, album) + n, err = suite.base.cache.Size(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, n) + album, err = suite.base.cache.Poll(suite.base.ctx, queue) + assert.Error(suite.T(), err) + assert.Equal(suite.T(), uint64(0x0), album) + n, err = suite.base.cache.Size(suite.base.ctx, queue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, n) + _, err = suite.base.cache.Poll(suite.base.ctx, queue) + assert.ErrorIs(suite.T(), err, domain.ErrUnknown) +} + +func (suite *RedisTestSuite) TestRedisPQueue() { + id, _ := GenId() + pqueue := id() + albumExp1 := id() + albumExp2 := id() + albumExp3 := id() + n, err := suite.base.cache.PSize(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, n) + err = suite.base.cache.PAdd(suite.base.ctx, pqueue, albumExp1, time.Unix(904867200, 0)) + assert.NoError(suite.T(), err) + err = suite.base.cache.PAdd(suite.base.ctx, pqueue, albumExp2, time.Unix(1075852800, 0)) + assert.NoError(suite.T(), err) + err = suite.base.cache.PAdd(suite.base.ctx, pqueue, albumExp3, time.Unix(681436800, 0)) + assert.NoError(suite.T(), err) + n, err = suite.base.cache.PSize(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 3, n) + album, expires, err := suite.base.cache.PPoll(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), albumExp3, album) + assert.True(suite.T(), expires.Equal(time.Unix(681436800, 0))) + n, err = suite.base.cache.PSize(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 2, n) + album, expires, err = suite.base.cache.PPoll(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), albumExp1, album) + assert.True(suite.T(), expires.Equal(time.Unix(904867200, 0))) + album, expires, err = suite.base.cache.PPoll(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), albumExp2, album) + assert.True(suite.T(), expires.Equal(time.Unix(1075852800, 0))) + n, err = suite.base.cache.PSize(suite.base.ctx, pqueue) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), 0, n) + _, _, err = suite.base.cache.PPoll(suite.base.ctx, pqueue) + assert.ErrorIs(suite.T(), err, domain.ErrUnknown) +} + +func (suite *RedisTestSuite) TestRedisPair() { + suite.base.TestPair() +} + +func (suite *RedisTestSuite) TestRedisToken() { + suite.base.TestToken() +} diff --git a/infrastructure/compressor/compressor.go b/infrastructure/compressor/compressor.go index 2d04c6f..37e9ace 100644 --- a/infrastructure/compressor/compressor.go +++ b/infrastructure/compressor/compressor.go @@ -1,24 +1,26 @@ package compressor import ( + "context" + "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/pkg/log" + "github.com/zitryss/aye-and-nay/internal/log" ) -func New(s string) (domain.Compresser, error) { - switch s { - case "imaginary": - log.Info("connecting to imaginary") - return NewImaginary() +func New(ctx context.Context, conf CompressorConfig) (domain.Compresser, error) { + switch conf.Compressor { case "shortpixel": - log.Info("connecting to compressor") - sp := NewShortPixel() - err := sp.Ping() + log.Info(context.Background(), "connecting to compressor") + sp := NewShortpixel(conf.Shortpixel) + err := sp.Ping(ctx) if err != nil { return nil, err } - sp.Monitor() + sp.Monitor(ctx) return sp, nil + case "imaginary": + log.Info(context.Background(), "connecting to imaginary") + return NewImaginary(ctx, conf.Imaginary) case "mock": return NewMock(), nil default: diff --git a/infrastructure/compressor/compressor_integration_test.go b/infrastructure/compressor/compressor_integration_test.go deleted file mode 100644 index 4f7add6..0000000 --- a/infrastructure/compressor/compressor_integration_test.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build integration - -package compressor - -import ( - "io" - "os" - "testing" - - "github.com/zitryss/aye-and-nay/internal/dockertest" - "github.com/zitryss/aye-and-nay/pkg/env" - "github.com/zitryss/aye-and-nay/pkg/log" -) - -func TestMain(m *testing.M) { - _, err := env.Lookup("CONTINUOUS_INTEGRATION") - if err != nil { - log.SetOutput(os.Stderr) - log.SetLevel(log.Lcritical) - docker := dockertest.New() - docker.RunImaginary() - log.SetOutput(io.Discard) - code := m.Run() - docker.Purge() - os.Exit(code) - } - code := m.Run() - os.Exit(code) -} diff --git a/infrastructure/compressor/compressor_test.go b/infrastructure/compressor/compressor_test.go new file mode 100644 index 0000000..fff35ee --- /dev/null +++ b/infrastructure/compressor/compressor_test.go @@ -0,0 +1,35 @@ +package compressor + +import ( + "flag" + "io" + "os" + "testing" + + "github.com/zitryss/aye-and-nay/internal/dockertest" + "github.com/zitryss/aye-and-nay/pkg/log" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestMain(m *testing.M) { + flag.Parse() + if *ci || !*integration { + code := m.Run() + os.Exit(code) + } + log.SetOutput(os.Stderr) + log.SetLevel(log.CRITICAL) + docker := dockertest.New() + host := &DefaultImaginaryConfig.Host + port := &DefaultImaginaryConfig.Port + docker.RunImaginary(host, port) + log.SetOutput(io.Discard) + code := m.Run() + docker.Purge() + os.Exit(code) +} diff --git a/infrastructure/compressor/config.go b/infrastructure/compressor/config.go index 851f5c3..4826dfa 100644 --- a/infrastructure/compressor/config.go +++ b/infrastructure/compressor/config.go @@ -2,54 +2,59 @@ package compressor import ( "time" - - "github.com/spf13/viper" ) -func newImaginaryConfig() imaginaryConfig { - return imaginaryConfig{ - host: viper.GetString("compressor.imaginary.host"), - port: viper.GetString("compressor.imaginary.port"), - times: viper.GetInt("compressor.imaginary.retry.times"), - pause: viper.GetDuration("compressor.imaginary.retry.pause"), - timeout: viper.GetDuration("compressor.imaginary.retry.timeout"), - } +type CompressorConfig struct { + Compressor string `mapstructure:"APP_COMPRESSOR" validate:"required"` + Shortpixel ShortpixelConfig `mapstructure:",squash"` + Imaginary ImaginaryConfig `mapstructure:",squash"` } -type imaginaryConfig struct { - host string - port string - times int - pause time.Duration - timeout time.Duration +type ShortpixelConfig struct { + Url string `mapstructure:"COMPRESSOR_SHORTPIXEL_URL" validate:"required"` + Url2 string `mapstructure:"COMPRESSOR_SHORTPIXEL_URL2" validate:"required"` + ApiKey string `mapstructure:"COMPRESSOR_SHORTPIXEL_API_KEY" validate:"required"` + RetryTimes int `mapstructure:"COMPRESSOR_SHORTPIXEL_RETRY_TIMES" validate:"required"` + RetryPause time.Duration `mapstructure:"COMPRESSOR_SHORTPIXEL_RETRY_PAUSE" validate:"required"` + Timeout time.Duration `mapstructure:"COMPRESSOR_SHORTPIXEL_TIMEOUT" validate:"required"` + Wait string `mapstructure:"COMPRESSOR_SHORTPIXEL_WAIT" validate:"required"` + UploadTimeout time.Duration `mapstructure:"COMPRESSOR_SHORTPIXEL_UPLOAD_TIMEOUT" validate:"required"` + DownloadTimeout time.Duration `mapstructure:"COMPRESSOR_SHORTPIXEL_DOWNLOAD_TIMEOUT" validate:"required"` + RepeatIn time.Duration `mapstructure:"COMPRESSOR_SHORTPIXEL_REPEAT_IN" validate:"required"` + RestartIn time.Duration `mapstructure:"COMPRESSOR_SHORTPIXEL_RESTART_IN" validate:"required"` } -func newShortPixelConfig() shortPixelConfig { - return shortPixelConfig{ - url: viper.GetString("compressor.shortpixel.url"), - url2: viper.GetString("compressor.shortpixel.url2"), - apiKey: viper.GetString("compressor.shortpixel.apiKey"), - times: viper.GetInt("compressor.shortpixel.retry.times"), - pause: viper.GetDuration("compressor.shortpixel.retry.pause"), - timeout: viper.GetDuration("compressor.shortpixel.retry.timeout"), - wait: viper.GetString("compressor.shortpixel.wait"), - uploadTimeout: viper.GetDuration("compressor.shortpixel.uploadTimeout"), - downloadTimeout: viper.GetDuration("compressor.shortpixel.downloadTimeout"), - repeatIn: viper.GetDuration("compressor.shortpixel.repeatIn"), - restartIn: viper.GetDuration("compressor.shortpixel.restartIn"), - } +type ImaginaryConfig struct { + Host string `mapstructure:"COMPRESSOR_IMAGINARY_HOST" validate:"required"` + Port string `mapstructure:"COMPRESSOR_IMAGINARY_PORT" validate:"required"` + RetryTimes int `mapstructure:"COMPRESSOR_IMAGINARY_RETRY_TIMES" validate:"required"` + RetryPause time.Duration `mapstructure:"COMPRESSOR_IMAGINARY_RETRY_PAUSE" validate:"required"` + Timeout time.Duration `mapstructure:"COMPRESSOR_IMAGINARY_TIMEOUT" validate:"required"` } -type shortPixelConfig struct { - url string - url2 string - apiKey string - times int - pause time.Duration - timeout time.Duration - wait string - uploadTimeout time.Duration - downloadTimeout time.Duration - repeatIn time.Duration - restartIn time.Duration +func (c CompressorConfig) IsMock() bool { + return c.Compressor == "mock" } + +var ( + DefaultShortpixelConfig = ShortpixelConfig{ + Url: "", + Url2: "", + ApiKey: "", + RetryTimes: 0, + RetryPause: 0, + Timeout: 0, + Wait: "", + UploadTimeout: 250 * time.Millisecond, + DownloadTimeout: 250 * time.Millisecond, + RepeatIn: 0, + RestartIn: 0, + } + DefaultImaginaryConfig = ImaginaryConfig{ + Host: "localhost", + Port: "9001", + RetryTimes: 4, + RetryPause: 5 * time.Second, + Timeout: 30 * time.Second, + } +) diff --git a/infrastructure/compressor/imaginary.go b/infrastructure/compressor/imaginary.go index 1bedbc3..eb6639d 100644 --- a/infrastructure/compressor/imaginary.go +++ b/infrastructure/compressor/imaginary.go @@ -1,12 +1,10 @@ package compressor import ( - "bytes" "context" "io" "mime/multipart" "net/http" - "os" "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" @@ -15,73 +13,46 @@ import ( "github.com/zitryss/aye-and-nay/pkg/retry" ) -func NewImaginary() (*Imaginary, error) { - conf := newImaginaryConfig() - ctx, cancel := context.WithTimeout(context.Background(), conf.timeout) +var ( + _ domain.Compresser = (*Imaginary)(nil) +) + +func NewImaginary(ctx context.Context, conf ImaginaryConfig) (*Imaginary, error) { + im := &Imaginary{conf} + ctx, cancel := context.WithTimeout(ctx, conf.Timeout) defer cancel() - err := retry.Do(conf.times, conf.pause, func() error { - url := "http://" + conf.host + ":" + conf.port + "/health" - body := io.Reader(nil) - req, err := http.NewRequestWithContext(ctx, "GET", url, body) - if err != nil { - return errors.Wrap(err) - } - c := http.Client{} - resp, err := c.Do(req) + err := retry.Do(conf.RetryTimes, conf.RetryPause, func() error { + _, err := im.Health(ctx) if err != nil { return errors.Wrap(err) } - _, err = io.Copy(io.Discard, resp.Body) - if err != nil { - _ = resp.Body.Close() - return errors.Wrap(err) - } - err = resp.Body.Close() - if err != nil { - return errors.Wrap(err) - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return errors.Wrap(errors.New("no connection to imaginary")) - } return nil }) if err != nil { return &Imaginary{}, errors.Wrap(err) } - return &Imaginary{conf}, nil + return im, nil } type Imaginary struct { - conf imaginaryConfig + conf ImaginaryConfig } func (im *Imaginary) Compress(ctx context.Context, f model.File) (model.File, error) { - defer func() { - switch v := f.Reader.(type) { - case *os.File: - _ = v.Close() - _ = os.Remove(v.Name()) - case multipart.File: - _ = v.Close() - case *bytes.Buffer: - pool.PutBuffer(v) - default: - panic(errors.Wrap(domain.ErrUnknown)) - } - }() - buf := pool.GetBuffer() + defer f.Close() + buf := pool.GetBufferN(f.Size) tee := model.File{ Reader: io.TeeReader(f.Reader, buf), Size: f.Size, } - body := pool.GetBuffer() + body := pool.GetBufferN(f.Size) defer pool.PutBuffer(body) multi := multipart.NewWriter(body) part, err := multi.CreateFormFile("file", "non-empty-field") if err != nil { return model.File{}, errors.Wrap(err) } - n, err := io.CopyN(part, tee, tee.Size) + n, err := io.Copy(part, tee.Reader) if err != nil { return model.File{}, errors.Wrap(err) } @@ -89,15 +60,15 @@ func (im *Imaginary) Compress(ctx context.Context, f model.File) (model.File, er if err != nil { return model.File{}, errors.Wrap(err) } - url := "http://" + im.conf.host + ":" + im.conf.port + "/convert?type=png&compression=9" - req, err := http.NewRequestWithContext(ctx, "POST", url, body) + url := "http://" + im.conf.Host + ":" + im.conf.Port + "/convert?type=png&compression=9" + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return model.File{}, errors.Wrap(err) } req.Header.Set("Content-Type", multi.FormDataContentType()) - c := http.Client{Timeout: im.conf.timeout} + c := http.Client{Timeout: im.conf.Timeout} resp := (*http.Response)(nil) - err = retry.Do(im.conf.times, im.conf.pause, func() error { + err = retry.Do(im.conf.RetryTimes, im.conf.RetryPause, func() error { resp, err = c.Do(req) if err != nil { return errors.Wrapf(domain.ErrThirdPartyUnavailable, "%s", err) @@ -107,7 +78,7 @@ func (im *Imaginary) Compress(ctx context.Context, f model.File) (model.File, er _ = resp.Body.Close() return errors.Wrap(domain.ErrUnsupportedMediaType) } - if resp.StatusCode < 200 || resp.StatusCode > 299 { + if resp.StatusCode/100 != 2 { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() return errors.Wrapf(domain.ErrThirdPartyUnavailable, "status code %d", resp.StatusCode) @@ -131,5 +102,36 @@ func (im *Imaginary) Compress(ctx context.Context, f model.File) (model.File, er if err != nil { return model.File{}, errors.Wrap(err) } - return model.File{Reader: buf, Size: n}, nil + closeFn := func() error { + pool.PutBuffer(buf) + return nil + } + return model.NewFile(buf, closeFn, n), nil +} + +func (im *Imaginary) Health(ctx context.Context) (bool, error) { + url := "http://" + im.conf.Host + ":" + im.conf.Port + "/health" + body := io.Reader(nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, body) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthCompressor, "%s", err) + } + c := http.Client{Timeout: im.conf.Timeout} + resp, err := c.Do(req) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthCompressor, "%s", err) + } + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + _ = resp.Body.Close() + return false, errors.Wrapf(domain.ErrBadHealthCompressor, "%s", err) + } + err = resp.Body.Close() + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthCompressor, "%s", err) + } + if resp.StatusCode/100 != 2 { + return false, errors.Wrapf(domain.ErrBadHealthCompressor, "%s", "no connection to imaginary") + } + return true, nil } diff --git a/infrastructure/compressor/imaginary_integration_test.go b/infrastructure/compressor/imaginary_integration_test.go index b82e111..3ac2af3 100644 --- a/infrastructure/compressor/imaginary_integration_test.go +++ b/infrastructure/compressor/imaginary_integration_test.go @@ -1,5 +1,3 @@ -//go:build integration - package compressor import ( @@ -8,12 +6,16 @@ import ( "os" "testing" - "github.com/zitryss/aye-and-nay/domain/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - _ "github.com/zitryss/aye-and-nay/internal/config" + "github.com/zitryss/aye-and-nay/domain/model" ) func TestImaginaryPositive(t *testing.T) { + if !*integration { + t.Skip() + } tests := []struct { filename string }{ @@ -32,49 +34,42 @@ func TestImaginaryPositive(t *testing.T) { } for _, tt := range tests { t.Run("", func(t *testing.T) { - im, err := NewImaginary() - if err != nil { - t.Fatal(err) - } + im, err := NewImaginary(context.Background(), DefaultImaginaryConfig) + require.NoError(t, err) b, err := os.ReadFile("../../testdata/" + tt.filename) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) buf := bytes.NewBuffer(b) - f := model.File{ - Reader: buf, - Size: int64(buf.Len()), - } + f := model.File{Reader: buf, Size: int64(buf.Len())} _, err = im.Compress(context.Background(), f) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) }) } } func TestImaginaryNegative(t *testing.T) { + if !*integration { + t.Skip() + } if testing.Short() { t.Skip("short flag is set") } - im, err := NewImaginary() - if err != nil { - t.Fatal(err) - } + im, err := NewImaginary(context.Background(), DefaultImaginaryConfig) + require.NoError(t, err) b, err := os.ReadFile("../../testdata/john.bmp") - if err != nil { - t.Error(err) - } + assert.NoError(t, err) buf := bytes.NewBuffer(b) - f1 := model.File{ - Reader: buf, - Size: int64(buf.Len()), - } + f1 := model.File{Reader: buf, Size: int64(buf.Len())} f2, err := im.Compress(context.Background(), f1) - if err != nil { - t.Error(err) - } - if f1.Size != f2.Size { - t.Error("f1.Size != f2.Size") + assert.NoError(t, err) + assert.Equal(t, f1.Size, f2.Size) +} + +func TestImaginaryHealth(t *testing.T) { + if !*integration { + t.Skip() } + im, err := NewImaginary(context.Background(), DefaultImaginaryConfig) + require.NoError(t, err) + _, err = im.Health(context.Background()) + assert.NoError(t, err) } diff --git a/infrastructure/compressor/mock.go b/infrastructure/compressor/mock.go index c217c16..22fd410 100644 --- a/infrastructure/compressor/mock.go +++ b/infrastructure/compressor/mock.go @@ -1,11 +1,8 @@ package compressor import ( - "bytes" "context" "io" - "mime/multipart" - "os" "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" @@ -13,6 +10,10 @@ import ( "github.com/zitryss/aye-and-nay/pkg/pool" ) +var ( + _ domain.Compresser = (*Mock)(nil) +) + func NewMock() *Mock { return &Mock{} } @@ -21,23 +22,19 @@ type Mock struct { } func (m *Mock) Compress(_ context.Context, f model.File) (model.File, error) { - defer func() { - switch v := f.Reader.(type) { - case *os.File: - _ = v.Close() - _ = os.Remove(v.Name()) - case multipart.File: - _ = v.Close() - case *bytes.Buffer: - pool.PutBuffer(v) - default: - panic(errors.Wrap(domain.ErrUnknown)) - } - }() - buf := pool.GetBuffer() - n, err := io.CopyN(buf, f, f.Size) + defer f.Close() + buf := pool.GetBufferN(f.Size) + n, err := io.Copy(buf, f.Reader) if err != nil { return model.File{}, errors.Wrap(err) } - return model.File{Reader: buf, Size: n}, nil + closeFn := func() error { + pool.PutBuffer(buf) + return nil + } + return model.NewFile(buf, closeFn, n), nil +} + +func (m *Mock) Health(_ context.Context) (bool, error) { + return true, nil } diff --git a/infrastructure/compressor/shortpixel.go b/infrastructure/compressor/shortpixel.go index 1063554..a8f56af 100644 --- a/infrastructure/compressor/shortpixel.go +++ b/infrastructure/compressor/shortpixel.go @@ -1,13 +1,11 @@ package compressor import ( - "bytes" "context" "encoding/json" "io" "mime/multipart" "net/http" - "os" "sync/atomic" "time" @@ -19,8 +17,11 @@ import ( "github.com/zitryss/aye-and-nay/pkg/retry" ) -func NewShortPixel(opts ...options) *Shortpixel { - conf := newShortPixelConfig() +var ( + _ domain.Compresser = (*Shortpixel)(nil) +) + +func NewShortpixel(conf ShortpixelConfig, opts ...options) *Shortpixel { sp := &Shortpixel{ conf: conf, ch: make(chan struct{}, 1), @@ -33,23 +34,23 @@ func NewShortPixel(opts ...options) *Shortpixel { type options func(*Shortpixel) -func WithHeartbeatRestart(ch chan<- interface{}) options { +func WithHeartbeatRestart(ch chan<- any) options { return func(sp *Shortpixel) { sp.heartbeat.restart = ch } } type Shortpixel struct { - conf shortPixelConfig + conf ShortpixelConfig done uint32 ch chan struct{} heartbeat struct { - restart chan<- interface{} + restart chan<- any } } -func (sp *Shortpixel) Ping() error { - ctx, cancel := context.WithTimeout(context.Background(), sp.conf.timeout) +func (sp *Shortpixel) Ping(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, sp.conf.Timeout) defer cancel() _, err := sp.upload(ctx, Png()) if err != nil { @@ -58,39 +59,40 @@ func (sp *Shortpixel) Ping() error { return nil } -func (sp *Shortpixel) Monitor() { +func (sp *Shortpixel) Monitor(ctx context.Context) { go func() { for { + select { + case <-ctx.Done(): + return + default: + } <-sp.ch if sp.heartbeat.restart != nil { - sp.heartbeat.restart <- struct{}{} + select { + case <-ctx.Done(): + return + case sp.heartbeat.restart <- struct{}{}: + } } - time.Sleep(sp.conf.restartIn) + time.Sleep(sp.conf.RestartIn) atomic.StoreUint32(&sp.done, 0) if sp.heartbeat.restart != nil { - sp.heartbeat.restart <- struct{}{} + select { + case <-ctx.Done(): + return + case sp.heartbeat.restart <- struct{}{}: + } } } }() } func (sp *Shortpixel) Compress(ctx context.Context, f model.File) (model.File, error) { - defer func() { - switch v := f.Reader.(type) { - case *os.File: - _ = v.Close() - _ = os.Remove(v.Name()) - case multipart.File: - _ = v.Close() - case *bytes.Buffer: - pool.PutBuffer(v) - default: - panic(errors.Wrap(domain.ErrUnknown)) - } - }() + defer f.Close() if atomic.LoadUint32(&sp.done) != 0 { - buf := pool.GetBuffer() - n, err := io.CopyN(buf, f, f.Size) + buf := pool.GetBufferN(f.Size) + n, err := io.Copy(buf, f.Reader) if err != nil { return model.File{}, errors.Wrap(err) } @@ -122,14 +124,14 @@ func (sp *Shortpixel) compress(ctx context.Context, f model.File) (model.File, e } func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) { - body := pool.GetBuffer() + body := pool.GetBufferN(f.Size) defer pool.PutBuffer(body) multi := multipart.NewWriter(body) part, err := multi.CreateFormField("key") if err != nil { return "", errors.Wrap(err) } - _, err = io.WriteString(part, sp.conf.apiKey) + _, err = io.WriteString(part, sp.conf.ApiKey) if err != nil { return "", errors.Wrap(err) } @@ -145,7 +147,7 @@ func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) if err != nil { return "", errors.Wrap(err) } - _, err = io.WriteString(part, sp.conf.wait) + _, err = io.WriteString(part, sp.conf.Wait) if err != nil { return "", errors.Wrap(err) } @@ -169,7 +171,7 @@ func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) if err != nil { return "", errors.Wrap(err) } - _, err = io.CopyN(part, f, f.Size) + _, err = io.Copy(part, f.Reader) if err != nil { return "", errors.Wrap(err) } @@ -177,19 +179,19 @@ func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) if err != nil { return "", errors.Wrap(err) } - c := http.Client{Timeout: sp.conf.uploadTimeout} - req, err := http.NewRequestWithContext(ctx, "POST", sp.conf.url, body) + c := http.Client{Timeout: sp.conf.UploadTimeout} + req, err := http.NewRequestWithContext(ctx, http.MethodPost, sp.conf.Url, body) if err != nil { return "", errors.Wrap(err) } req.Header.Set("Content-Type", multi.FormDataContentType()) resp := (*http.Response)(nil) - err = retry.Do(sp.conf.times, sp.conf.pause, func() error { + err = retry.Do(sp.conf.RetryTimes, sp.conf.RetryPause, func() error { resp, err = c.Do(req) if err != nil { return errors.Wrapf(domain.ErrThirdPartyUnavailable, "%s", err) } - if resp.StatusCode < 200 || resp.StatusCode > 299 { + if resp.StatusCode/100 != 2 { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() return errors.Wrapf(domain.ErrThirdPartyUnavailable, "status code %d", resp.StatusCode) @@ -199,7 +201,7 @@ func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) if err != nil { return "", errors.Wrap(err) } - buf := pool.GetBuffer() + buf := pool.GetBufferN(resp.ContentLength) defer pool.PutBuffer(buf) _, err = io.Copy(buf, resp.Body) if err != nil { @@ -218,7 +220,7 @@ func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) buf.Write(bb) response := struct { Status struct { - Code interface{} + Code any Message string } OriginalUrl string @@ -252,7 +254,7 @@ func (sp *Shortpixel) upload(ctx context.Context, f model.File) (string, error) } func (sp *Shortpixel) repeat(ctx context.Context, src string) (string, error) { - time.Sleep(sp.conf.repeatIn) + time.Sleep(sp.conf.RepeatIn) body := pool.GetBuffer() defer pool.PutBuffer(body) request := struct { @@ -262,9 +264,9 @@ func (sp *Shortpixel) repeat(ctx context.Context, src string) (string, error) { Convertto string `json:"convertto"` Urllist []string `json:"urllist"` }{ - Key: sp.conf.apiKey, + Key: sp.conf.ApiKey, Lossy: "1", - Wait: sp.conf.wait, + Wait: sp.conf.Wait, Convertto: "png", Urllist: []string{src}, } @@ -272,18 +274,18 @@ func (sp *Shortpixel) repeat(ctx context.Context, src string) (string, error) { if err != nil { return "", errors.Wrap(err) } - c := http.Client{Timeout: sp.conf.uploadTimeout} - req, err := http.NewRequestWithContext(ctx, "POST", sp.conf.url2, body) + c := http.Client{Timeout: sp.conf.UploadTimeout} + req, err := http.NewRequestWithContext(ctx, http.MethodPost, sp.conf.Url2, body) if err != nil { return "", errors.Wrap(err) } resp := (*http.Response)(nil) - err = retry.Do(sp.conf.times, sp.conf.pause, func() error { + err = retry.Do(sp.conf.RetryTimes, sp.conf.RetryPause, func() error { resp, err = c.Do(req) if err != nil { return errors.Wrapf(domain.ErrThirdPartyUnavailable, "%s", err) } - if resp.StatusCode < 200 || resp.StatusCode > 299 { + if resp.StatusCode/100 != 2 { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() return errors.Wrapf(domain.ErrThirdPartyUnavailable, "status code %d", resp.StatusCode) @@ -293,7 +295,7 @@ func (sp *Shortpixel) repeat(ctx context.Context, src string) (string, error) { if err != nil { return "", errors.Wrap(err) } - buf := pool.GetBuffer() + buf := pool.GetBufferN(resp.ContentLength) defer pool.PutBuffer(buf) _, err = io.Copy(buf, resp.Body) if err != nil { @@ -312,7 +314,7 @@ func (sp *Shortpixel) repeat(ctx context.Context, src string) (string, error) { buf.Write(b) response := struct { Status struct { - Code interface{} + Code any Message string } LossyUrl string @@ -338,19 +340,19 @@ func (sp *Shortpixel) repeat(ctx context.Context, src string) (string, error) { } func (sp *Shortpixel) download(ctx context.Context, src string) (model.File, error) { - c := http.Client{Timeout: sp.conf.downloadTimeout} + c := http.Client{Timeout: sp.conf.DownloadTimeout} body := io.Reader(nil) - req, err := http.NewRequestWithContext(ctx, "GET", src, body) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, src, body) if err != nil { return model.File{}, errors.Wrap(err) } resp := (*http.Response)(nil) - err = retry.Do(sp.conf.times, sp.conf.pause, func() error { + err = retry.Do(sp.conf.RetryTimes, sp.conf.RetryPause, func() error { resp, err = c.Do(req) if err != nil { return errors.Wrapf(domain.ErrThirdPartyUnavailable, "%s", err) } - if resp.StatusCode < 200 || resp.StatusCode > 299 { + if resp.StatusCode/100 != 2 { _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() return errors.Wrapf(domain.ErrThirdPartyUnavailable, "status code %d", resp.StatusCode) @@ -360,7 +362,7 @@ func (sp *Shortpixel) download(ctx context.Context, src string) (model.File, err if err != nil { return model.File{}, errors.Wrap(err) } - buf := pool.GetBuffer() + buf := pool.GetBufferN(resp.ContentLength) n, err := io.Copy(buf, resp.Body) if err != nil { _, _ = io.Copy(io.Discard, resp.Body) @@ -371,5 +373,17 @@ func (sp *Shortpixel) download(ctx context.Context, src string) (model.File, err if err != nil { return model.File{}, errors.Wrap(err) } - return model.File{Reader: buf, Size: n}, nil + closeFn := func() error { + pool.PutBuffer(buf) + return nil + } + return model.NewFile(buf, closeFn, n), nil +} + +func (sp *Shortpixel) Health(ctx context.Context) (bool, error) { + err := sp.Ping(ctx) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthCompressor, "%s", err) + } + return true, nil } diff --git a/infrastructure/compressor/shortpixel_test.go b/infrastructure/compressor/shortpixel_test.go index b63b74a..8e27330 100644 --- a/infrastructure/compressor/shortpixel_test.go +++ b/infrastructure/compressor/shortpixel_test.go @@ -1,5 +1,3 @@ -//go:build unit - package compressor import ( @@ -11,17 +9,15 @@ import ( "testing" "time" - "github.com/spf13/viper" + "github.com/stretchr/testify/assert" "github.com/zitryss/aye-and-nay/domain/domain" - _ "github.com/zitryss/aye-and-nay/internal/config" . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" ) type response []struct { Status struct { - Code interface{} + Code any Message string } OriginalURL string `json:"OriginalURL,omitempty"` @@ -42,13 +38,14 @@ type response []struct { LocalPath string `json:"LocalPath,omitempty"` } -func TestShortPixel(t *testing.T) { +func TestShortpixel(t *testing.T) { + if !*unit { + t.Skip() + } t.Run("Positive1", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { _, err := io.Copy(w, Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -56,7 +53,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "2", @@ -81,25 +78,20 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) }) t.Run("Positive2", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { _, err := io.Copy(w, Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -129,30 +121,26 @@ func TestShortPixel(t *testing.T) { } ]` _, err := io.WriteString(w, resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) }) t.Run("NegativeInvalidUrl1", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeInvalidUrl2", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -163,7 +151,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "2", @@ -188,37 +176,32 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeTimeout1", func(t *testing.T) { - d := viper.GetDuration("compressor.shortpixel.uploadTimeout") + viper.GetDuration("compressor.shortpixel.downloadTimeout") + conf := DefaultShortpixelConfig fn1 := func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * d) + time.Sleep(2 * (conf.UploadTimeout + conf.DownloadTimeout)) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeTimeout2", func(t *testing.T) { - d := viper.GetDuration("compressor.shortpixel.uploadTimeout") + viper.GetDuration("compressor.shortpixel.downloadTimeout") + conf := DefaultShortpixelConfig fn1 := func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * d) + time.Sleep(2 * (conf.UploadTimeout + conf.DownloadTimeout)) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -226,7 +209,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "2", @@ -251,18 +234,14 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - sp := NewShortPixel() + conf.Url = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeErrorHttpCode1", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -270,12 +249,11 @@ func TestShortPixel(t *testing.T) { } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeErrorHttpCode2", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -287,7 +265,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "2", @@ -312,18 +290,15 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeInvalidJson", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -351,25 +326,22 @@ func TestShortPixel(t *testing.T) { } ` _, err := io.WriteString(w, resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeErrorStatusCode1", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: -110, @@ -378,25 +350,22 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp[0]) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeErrorStatusCode2", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: -201, @@ -405,25 +374,22 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp[0]) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrNotImage) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrNotImage) }) t.Run("NegativeErrorStatusCode3", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: -202, @@ -432,25 +398,20 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp[0]) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() - viper.Set("compressor.shortpixel.url", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrNotImage) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrNotImage) }) t.Run("PositiveRecovery1", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { _, err := io.Copy(w, Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -458,7 +419,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "2", @@ -483,9 +444,7 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() @@ -493,7 +452,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -505,26 +464,21 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver3 := httptest.NewServer(http.HandlerFunc(fn3)) defer mockserver3.Close() - viper.Set("compressor.shortpixel.url", mockserver3.URL) - viper.Set("compressor.shortpixel.url2", mockserver2.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver3.URL + conf.Url2 = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) }) t.Run("PositiveRecovery2", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { _, err := io.Copy(w, Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -554,9 +508,7 @@ func TestShortPixel(t *testing.T) { } ]` _, err := io.WriteString(w, resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() @@ -573,19 +525,16 @@ func TestShortPixel(t *testing.T) { } ]` _, err := io.WriteString(w, resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver3 := httptest.NewServer(http.HandlerFunc(fn3)) defer mockserver3.Close() - viper.Set("compressor.shortpixel.url", mockserver3.URL) - viper.Set("compressor.shortpixel.url2", mockserver2.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver3.URL + conf.Url2 = mockserver2.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) }) t.Run("NegativeRecoveryInvalidUrl", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -596,7 +545,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -608,24 +557,21 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - viper.Set("compressor.shortpixel.url2", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + conf.Url2 = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeRecoveryTimeout", func(t *testing.T) { - d := viper.GetDuration("compressor.shortpixel.uploadTimeout") + viper.GetDuration("compressor.shortpixel.downloadTimeout") + conf := DefaultShortpixelConfig fn1 := func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * d) + time.Sleep(2 * (conf.UploadTimeout + conf.DownloadTimeout)) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -633,7 +579,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -645,19 +591,15 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - viper.Set("compressor.shortpixel.url2", mockserver1.URL) - sp := NewShortPixel() + conf.Url = mockserver2.URL + conf.Url2 = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeRecoveryErrorHttpCode", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -669,7 +611,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -681,19 +623,16 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - viper.Set("compressor.shortpixel.url2", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + conf.Url2 = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeRecoveryInvalidJson", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { @@ -721,9 +660,7 @@ func TestShortPixel(t *testing.T) { } ` _, err := io.WriteString(w, resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -731,7 +668,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -743,26 +680,23 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - viper.Set("compressor.shortpixel.url2", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + conf.Url2 = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeRecoveryErrorStatusCode", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: -110, @@ -771,9 +705,7 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp[0]) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -781,7 +713,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -793,26 +725,23 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - viper.Set("compressor.shortpixel.url2", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + conf.Url2 = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) t.Run("NegativeRecoveryProcessingStatusCode", func(t *testing.T) { fn1 := func(w http.ResponseWriter, r *http.Request) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -824,9 +753,7 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp[0]) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver1 := httptest.NewServer(http.HandlerFunc(fn1)) defer mockserver1.Close() @@ -834,7 +761,7 @@ func TestShortPixel(t *testing.T) { resp := response{ { Status: struct { - Code interface{} + Code any Message string }{ Code: "1", @@ -846,18 +773,15 @@ func TestShortPixel(t *testing.T) { }, } err := json.NewEncoder(w).Encode(resp) - if err != nil { - t.Error(err) - } + assert.NoError(t, err) } mockserver2 := httptest.NewServer(http.HandlerFunc(fn2)) defer mockserver2.Close() - viper.Set("compressor.shortpixel.url", mockserver2.URL) - viper.Set("compressor.shortpixel.url2", mockserver1.URL) - sp := NewShortPixel() + conf := DefaultShortpixelConfig + conf.Url = mockserver2.URL + conf.Url2 = mockserver1.URL + sp := NewShortpixel(conf) _, err := sp.Compress(context.Background(), Png()) - if !errors.Is(err, domain.ErrThirdPartyUnavailable) { - t.Error(err) - } + assert.ErrorIs(t, err, domain.ErrThirdPartyUnavailable) }) } diff --git a/infrastructure/database/badger.go b/infrastructure/database/badger.go index ff7ae81..37e9300 100644 --- a/infrastructure/database/badger.go +++ b/infrastructure/database/badger.go @@ -5,12 +5,12 @@ import ( "encoding/binary" "encoding/gob" "runtime" - "sort" "time" "github.com/dgraph-io/badger/v3" "github.com/dgraph-io/badger/v3/options" lru "github.com/hashicorp/golang-lru" + "golang.org/x/exp/slices" "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" @@ -18,26 +18,22 @@ import ( "github.com/zitryss/aye-and-nay/pkg/pool" ) -type mode bool - -const ( - disk mode = false - inMemory mode = true +var ( + _ domain.Databaser = (*Badger)(nil) ) -func NewBadger(mode mode) (*Badger, error) { +func NewBadger(conf BadgerConfig) (*Badger, error) { _ = runtime.GOMAXPROCS(128) - conf := newBadgerConfig() path := "./badger" - if mode == inMemory { + if conf.InMemory { path = "" } - opts := badger.DefaultOptions(path).WithCompression(options.None).WithLogger(nil).WithInMemory(bool(mode)) + opts := badger.DefaultOptions(path).WithCompression(options.None).WithLogger(nil).WithInMemory(conf.InMemory) db, err := badger.Open(opts) if err != nil { return nil, errors.Wrap(err) } - cache, err := lru.New(conf.lru) + cache, err := lru.New(conf.LRU) if err != nil { return &Badger{}, errors.Wrap(err) } @@ -45,16 +41,21 @@ func NewBadger(mode mode) (*Badger, error) { } type Badger struct { - conf badgerConfig + conf BadgerConfig db *badger.DB cache *lru.Cache } -func (b *Badger) Monitor() { +func (b *Badger) Monitor(ctx context.Context) { go func() { for { - _ = b.db.RunValueLogGC(b.conf.gcRatio) - time.Sleep(b.conf.cleanupInterval) + select { + case <-ctx.Done(): + return + default: + } + _ = b.db.RunValueLogGC(b.conf.GcRatio) + time.Sleep(b.conf.CleanupInterval) } }() } @@ -68,7 +69,7 @@ func (b *Badger) SaveAlbum(_ context.Context, alb model.Album) error { albLru := make(albumLru, len(alb.Images)) for i := range alb.Images { img := &alb.Images[i] - img.Compressed = b.conf.compressed + img.Compressed = b.conf.Compressed edgs[img.Id] = make(map[uint64]int, len(alb.Images)) albLru[img.Id] = img.Src } @@ -198,7 +199,7 @@ func (b *Badger) GetImagesOrdered(_ context.Context, album uint64) ([]model.Imag if errors.Is(err, badger.ErrKeyNotFound) { return nil, errors.Wrap(domain.ErrAlbumNotFound) } - sort.Slice(alb.Images, func(i, j int) bool { return alb.Images[i].Rating > alb.Images[j].Rating }) + slices.SortFunc(alb.Images, func(a, b model.Image) bool { return a.Rating > b.Rating }) return alb.Images, nil } @@ -345,10 +346,23 @@ func (b *Badger) lruAdd(album uint64) error { return nil } -func (b *Badger) Close() error { +func (b *Badger) Health(_ context.Context) (bool, error) { + return true, nil +} + +func (b *Badger) Close(_ context.Context) error { err := b.db.Close() if err != nil { return errors.Wrap(err) } return nil } + +func (b *Badger) Reset() error { + err := b.db.DropAll() + if err != nil { + return errors.Wrap(err) + } + b.cache.Purge() + return nil +} diff --git a/infrastructure/database/badger_integration_test.go b/infrastructure/database/badger_integration_test.go new file mode 100644 index 0000000..e9a635b --- /dev/null +++ b/infrastructure/database/badger_integration_test.go @@ -0,0 +1,90 @@ +package database + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + . "github.com/zitryss/aye-and-nay/internal/generator" +) + +func TestBadgerTestSuite(t *testing.T) { + suite.Run(t, &BadgerTestSuite{}) +} + +type BadgerTestSuite struct { + suite.Suite + base MemTestSuite +} + +func (suite *BadgerTestSuite) SetupSuite() { + if !*integration { + suite.T().Skip() + } + suite.base = MemTestSuite{} + suite.base.SetT(suite.T()) + ctx, cancel := context.WithCancel(context.Background()) + badger, err := NewBadger(DefaultBadgerConfig) + require.NoError(suite.T(), err) + suite.base.ctx = ctx + suite.base.cancel = cancel + suite.base.db = badger + suite.base.setupTestFn = suite.SetupTest +} + +func (suite *BadgerTestSuite) SetupTest() { + err := suite.base.db.(*Badger).Reset() + require.NoError(suite.T(), err) +} + +func (suite *BadgerTestSuite) TearDownTest() { + +} + +func (suite *BadgerTestSuite) TearDownSuite() { + err := suite.base.db.(*Badger).Reset() + require.NoError(suite.T(), err) + err = suite.base.db.(*Badger).Close(suite.base.ctx) + require.NoError(suite.T(), err) + suite.base.cancel() +} + +func (suite *BadgerTestSuite) TestBadgerAlbum() { + suite.base.TestAlbum() +} + +func (suite *BadgerTestSuite) TestBadgerCount() { + suite.base.TestCount() +} + +func (suite *BadgerTestSuite) TestBadgerImage() { + suite.base.TestImage() +} + +func (suite *BadgerTestSuite) TestBadgerVote() { + suite.base.TestVote() +} + +func (suite *BadgerTestSuite) TestBadgerSort() { + suite.base.TestSort() +} + +func (suite *BadgerTestSuite) TestBadgerRatings() { + suite.base.TestRatings() +} + +func (suite *BadgerTestSuite) TestBadgerDelete() { + suite.base.TestDelete() +} + +func (suite *BadgerTestSuite) TestBadgerLru() { + id, ids := GenId() + alb1 := suite.base.saveAlbum(id, ids) + _ = suite.base.saveAlbum(id, ids) + edgs, err := suite.base.db.GetEdges(suite.base.ctx, ids.Uint64(0)) + assert.NoError(suite.base.T(), err) + assert.Equal(suite.base.T(), alb1.Edges, edgs) +} diff --git a/infrastructure/database/badger_test.go b/infrastructure/database/badger_test.go deleted file mode 100644 index 88ff048..0000000 --- a/infrastructure/database/badger_test.go +++ /dev/null @@ -1,489 +0,0 @@ -//go:build unit - -package database - -import ( - "context" - "reflect" - "sort" - "testing" - "time" - - "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/domain/model" - _ "github.com/zitryss/aye-and-nay/internal/config" - . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" -) - -func TestBadgerAlbum(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x6CC4) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - edgs, err := badger.GetEdges(context.Background(), 0x6CC4) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(edgs, alb.Edges) { - t.Error("edgs != alb.GetEdges") - } - }) - t.Run("Negative1", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0xA566) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - alb = AlbumFullFactory(0xA566) - err = badger.SaveAlbum(context.Background(), alb) - if !errors.Is(err, domain.ErrAlbumAlreadyExists) { - t.Error(err) - } - }) - t.Run("Negative2", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - _, err = badger.GetImagesIds(context.Background(), 0xA9B4) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative3", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - _, err = badger.GetEdges(context.Background(), 0x3F1E) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerCount(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x746C) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - n, err := badger.CountImages(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - n, err = badger.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - err = badger.UpdateCompressionStatus(context.Background(), 0x746C, 0x3E3D) - if err != nil { - t.Error(err) - } - n, err = badger.CountImages(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - n, err = badger.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 1 { - t.Error("n != 1") - } - err = badger.UpdateCompressionStatus(context.Background(), 0x746C, 0xB399) - if err != nil { - t.Error(err) - } - n, err = badger.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 2 { - t.Error("n != 2") - } - }) - t.Run("Negative1", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x99DF) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = badger.UpdateCompressionStatus(context.Background(), 0x99DF, 0x3E3D) - if err != nil { - t.Error(err) - } - err = badger.UpdateCompressionStatus(context.Background(), 0x99DF, 0x3E3D) - if err != nil { - t.Error(err) - } - n, err := badger.CountImagesCompressed(context.Background(), 0x99DF) - if err != nil { - t.Error(err) - } - if n != 1 { - t.Error("n != 1") - } - }) - t.Run("Negative2", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - _, err = badger.CountImages(context.Background(), 0xF256) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative3", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - _, err = badger.CountImagesCompressed(context.Background(), 0xC52A) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative4", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - err = badger.UpdateCompressionStatus(context.Background(), 0xF73E, 0x3E3D) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative5", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0xDF75) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = badger.UpdateCompressionStatus(context.Background(), 0xDF75, 0xE7A4) - if !errors.Is(err, domain.ErrImageNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerImage(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0xB0C4) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - src, err := badger.GetImageSrc(context.Background(), 0xB0C4, 0x51DE) - if err != nil { - t.Error(err) - } - if src != "/aye-and-nay/albums/xLAAAAAAAAA/images/3lEAAAAAAAA" { - t.Error("src != \"/aye-and-nay/albums/xLAAAAAAAAA/images/3lEAAAAAAAA\"") - } - }) - t.Run("Negative1", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - _, err = badger.GetImageSrc(context.Background(), 0x12EE, 0x51DE) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative2", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0xD585) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - _, err = badger.GetImageSrc(context.Background(), 0xD585, 0xDA30) - if !errors.Is(err, domain.ErrImageNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerVote(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0x4D76) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = badger.SaveVote(context.Background(), 0x4D76, 0xDA2A, 0xDA52) - if err != nil { - t.Error(err) - } - err = badger.SaveVote(context.Background(), 0x4D76, 0xDA2A, 0xDA52) - if err != nil { - t.Error(err) - } - edgs, err := badger.GetEdges(context.Background(), 0x4D76) - if err != nil { - t.Error(err) - } - if edgs[0xDA2A][0xDA52] != 2 { - t.Error("edgs[imageFrom][imageTo] != 2") - } - }) - t.Run("Negative", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - err = badger.SaveVote(context.Background(), 0x1FAD, 0x84E6, 0x308E) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerSort(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0x5A96) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - imgs1, err := badger.GetImagesOrdered(context.Background(), 0x5A96) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/3lEAAAAAAAA", Rating: 0.77920413} - img2 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/PT4AAAAAAAA", Rating: 0.48954984} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/KtoAAAAAAAA", Rating: 0.41218211} - img4 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/mbMAAAAAAAA", Rating: 0.19186324} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/UtoAAAAAAAA", Rating: 0.13278389} - imgs2 := []model.Image{img1, img2, img3, img4, img5} - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } - }) - t.Run("Negative", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - _, err = badger.GetImagesOrdered(context.Background(), 0x66BE) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerRatings(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0x4E54) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/PT4AAAAAAAA", Rating: 0.54412788} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/mbMAAAAAAAA", Rating: 0.32537162} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/KtoAAAAAAAA", Rating: 0.43185491} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/3lEAAAAAAAA", Rating: 0.57356209} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/UtoAAAAAAAA", Rating: 0.61438023} - imgs1 := []model.Image{img1, img2, img3, img4, img5} - vector := map[uint64]float64{} - vector[img1.Id] = img1.Rating - vector[img2.Id] = img2.Rating - vector[img3.Id] = img3.Rating - vector[img4.Id] = img4.Rating - vector[img5.Id] = img5.Rating - err = badger.UpdateRatings(context.Background(), 0x4E54, vector) - if err != nil { - t.Error(err) - } - imgs2, err := badger.GetImagesOrdered(context.Background(), 0x4E54) - if err != nil { - t.Error(err) - } - sort.Slice(imgs1, func(i, j int) bool { return imgs1[i].Rating > imgs1[j].Rating }) - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } - }) - t.Run("Negative", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/PT4AAAAAAAA", Rating: 0.54412788} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/mbMAAAAAAAA", Rating: 0.32537162} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/KtoAAAAAAAA", Rating: 0.43185491} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/3lEAAAAAAAA", Rating: 0.57356209} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/UtoAAAAAAAA", Rating: 0.61438023} - vector := map[uint64]float64{} - vector[img1.Id] = img1.Rating - vector[img2.Id] = img2.Rating - vector[img3.Id] = img3.Rating - vector[img4.Id] = img4.Rating - vector[img5.Id] = img5.Rating - err = badger.UpdateRatings(context.Background(), 0xA293, vector) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerDelete(t *testing.T) { - t.Run("Positive1", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x748C) - _, err = badger.CountImages(context.Background(), 0x748C) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - n, err := badger.CountImages(context.Background(), 0x748C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - albums, err := badger.AlbumsToBeDeleted(context.Background()) - if err != nil { - t.Error(err) - } - if albums != nil { - t.Error("albums != nil") - } - err = badger.DeleteAlbum(context.Background(), 0x748C) - if err != nil { - t.Error(err) - } - _, err = badger.CountImages(context.Background(), 0x748C) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Positive2", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x7B43) - alb.Expires = time.Now().Add(-1 * time.Hour) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - albums, err := badger.AlbumsToBeDeleted(context.Background()) - if err != nil { - t.Error(err) - } - if !(len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero()) { - t.Error("!(len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero())") - } - err = badger.DeleteAlbum(context.Background(), 0x7B43) - if err != nil { - t.Error(err) - } - _, err = badger.CountImages(context.Background(), 0x7B43) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative", func(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x608C) - err = badger.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = badger.DeleteAlbum(context.Background(), 0xB7FF) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) -} - -func TestBadgerLru(t *testing.T) { - badger, err := NewBadger(inMemory) - if err != nil { - t.Fatal(err) - } - alb1 := AlbumEmptyFactory(0x36FC) - err = badger.SaveAlbum(context.Background(), alb1) - if err != nil { - t.Error(err) - } - alb2 := AlbumEmptyFactory(0xB020) - err = badger.SaveAlbum(context.Background(), alb2) - if err != nil { - t.Error(err) - } - edgs, err := badger.GetEdges(context.Background(), 0x36FC) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(edgs, alb1.Edges) { - t.Error("edgs != alb1.GetEdges") - } -} diff --git a/infrastructure/database/config.go b/infrastructure/database/config.go index 4912af4..1f5924b 100644 --- a/infrastructure/database/config.go +++ b/infrastructure/database/config.go @@ -2,54 +2,55 @@ package database import ( "time" - - "github.com/spf13/viper" ) -func newMemConfig() memConfig { - return memConfig{ - compressed: viper.GetString("compressor.use") == "mock", - } +type DatabaseConfig struct { + Database string `mapstructure:"APP_DATABASE" validate:"required"` + Mem MemConfig `mapstructure:",squash"` + Mongo MongoConfig `mapstructure:",squash"` + Badger BadgerConfig `mapstructure:",squash"` } -type memConfig struct { - compressed bool +type MemConfig struct { + Compressed bool } -func newMongoConfig() mongoConfig { - return mongoConfig{ - host: viper.GetString("database.mongo.host"), - port: viper.GetString("database.mongo.port"), - times: viper.GetInt("database.mongo.retry.times"), - pause: viper.GetDuration("database.mongo.retry.pause"), - timeout: viper.GetDuration("database.mongo.retry.timeout"), - compressed: viper.GetString("compressor.use") == "mock", - lru: viper.GetInt("database.mongo.lru"), - } +type MongoConfig struct { + Host string `mapstructure:"DATABASE_MONGO_HOST" validate:"required"` + Port string `mapstructure:"DATABASE_MONGO_PORT" validate:"required"` + RetryTimes int `mapstructure:"DATABASE_MONGO_RETRY_TIMES" validate:"required"` + RetryPause time.Duration `mapstructure:"DATABASE_MONGO_RETRY_PAUSE" validate:"required"` + Timeout time.Duration `mapstructure:"DATABASE_MONGO_TIMEOUT" validate:"required"` + LRU int `mapstructure:"DATABASE_MONGO_LRU" validate:"required"` + Compressed bool } -type mongoConfig struct { - host string - port string - times int - pause time.Duration - timeout time.Duration - compressed bool - lru int +type BadgerConfig struct { + InMemory bool `mapstructure:"DATABASE_BADGER_IN_MEMORY"` + GcRatio float64 `mapstructure:"DATABASE_BADGER_GC_RATIO" validate:"required"` + CleanupInterval time.Duration `mapstructure:"DATABASE_BADGER_CLEANUP_INTERVAL" validate:"required"` + LRU int `mapstructure:"DATABASE_BADGER_LRU" validate:"required"` + Compressed bool } -func newBadgerConfig() badgerConfig { - return badgerConfig{ - gcRatio: viper.GetFloat64("database.badger.gcRatio"), - cleanupInterval: viper.GetDuration("database.badger.cleanupInterval"), - compressed: viper.GetString("compressor.use") == "mock", - lru: viper.GetInt("database.badger.lru"), +var ( + DefaultMemConfig = MemConfig{ + Compressed: false, } -} - -type badgerConfig struct { - gcRatio float64 - cleanupInterval time.Duration - compressed bool - lru int -} + DefaultMongoConfig = MongoConfig{ + Host: "localhost", + Port: "27017", + RetryTimes: 4, + RetryPause: 5 * time.Second, + Timeout: 30 * time.Second, + LRU: 1, + Compressed: false, + } + DefaultBadgerConfig = BadgerConfig{ + InMemory: true, + GcRatio: 0, + CleanupInterval: 0, + LRU: 1, + Compressed: false, + } +) diff --git a/infrastructure/database/database.go b/infrastructure/database/database.go index 67b21fe..9facaae 100644 --- a/infrastructure/database/database.go +++ b/infrastructure/database/database.go @@ -1,25 +1,28 @@ package database import ( + "context" + "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/pkg/log" + "github.com/zitryss/aye-and-nay/internal/log" ) -func New(s string) (domain.Databaser, error) { - switch s { +func New(ctx context.Context, conf DatabaseConfig) (domain.Databaser, error) { + switch conf.Database { case "mongo": - log.Info("connecting to database") - return NewMongo() + log.Info(context.Background(), "connecting to database") + return NewMongo(ctx, conf.Mongo) case "badger": - log.Info("connecting to embedded database") - b, err := NewBadger(disk) + log.Info(context.Background(), "connecting to embedded database") + b, err := NewBadger(conf.Badger) if err != nil { return nil, err } + b.Monitor(ctx) return b, nil case "mem": - return NewMem(), nil + return NewMem(conf.Mem), nil default: - return NewMem(), nil + return NewMem(conf.Mem), nil } } diff --git a/infrastructure/database/database_integration_test.go b/infrastructure/database/database_integration_test.go deleted file mode 100644 index acc37f4..0000000 --- a/infrastructure/database/database_integration_test.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build integration - -package database - -import ( - "io" - "os" - "testing" - - "github.com/zitryss/aye-and-nay/internal/dockertest" - "github.com/zitryss/aye-and-nay/pkg/env" - "github.com/zitryss/aye-and-nay/pkg/log" -) - -func TestMain(m *testing.M) { - _, err := env.Lookup("CONTINUOUS_INTEGRATION") - if err != nil { - log.SetOutput(os.Stderr) - log.SetLevel(log.Lcritical) - docker := dockertest.New() - docker.RunMongo() - log.SetOutput(io.Discard) - code := m.Run() - docker.Purge() - os.Exit(code) - } - code := m.Run() - os.Exit(code) -} diff --git a/infrastructure/database/database_test.go b/infrastructure/database/database_test.go new file mode 100644 index 0000000..0d7c1eb --- /dev/null +++ b/infrastructure/database/database_test.go @@ -0,0 +1,35 @@ +package database + +import ( + "flag" + "io" + "os" + "testing" + + "github.com/zitryss/aye-and-nay/internal/dockertest" + "github.com/zitryss/aye-and-nay/pkg/log" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestMain(m *testing.M) { + flag.Parse() + if *ci || !*integration { + code := m.Run() + os.Exit(code) + } + log.SetOutput(os.Stderr) + log.SetLevel(log.CRITICAL) + docker := dockertest.New() + host := &DefaultMongoConfig.Host + port := &DefaultMongoConfig.Port + docker.RunMongo(host, port) + log.SetOutput(io.Discard) + code := m.Run() + docker.Purge() + os.Exit(code) +} diff --git a/infrastructure/database/mem.go b/infrastructure/database/mem.go index 34a6bc8..2dca703 100644 --- a/infrastructure/database/mem.go +++ b/infrastructure/database/mem.go @@ -2,16 +2,20 @@ package database import ( "context" - "sort" "sync" + "golang.org/x/exp/slices" + "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" "github.com/zitryss/aye-and-nay/pkg/errors" ) -func NewMem() *Mem { - conf := newMemConfig() +var ( + _ domain.Databaser = (*Mem)(nil) +) + +func NewMem(conf MemConfig) *Mem { return &Mem{ conf: conf, syncAlbums: syncAlbums{albums: map[uint64]model.Album{}}, @@ -19,7 +23,7 @@ func NewMem() *Mem { } type Mem struct { - conf memConfig + conf MemConfig syncAlbums } @@ -38,7 +42,7 @@ func (m *Mem) SaveAlbum(_ context.Context, alb model.Album) error { edgs := make(map[uint64]map[uint64]int, len(alb.Images)) for i := range alb.Images { img := &alb.Images[i] - img.Compressed = m.conf.compressed + img.Compressed = m.conf.Compressed edgs[img.Id] = make(map[uint64]int, len(alb.Images)) } alb.Edges = edgs @@ -189,7 +193,7 @@ func (m *Mem) GetImagesOrdered(_ context.Context, album uint64) ([]model.Image, } imgs := make([]model.Image, len(alb.Images)) copy(imgs, alb.Images) - sort.Slice(imgs, func(i, j int) bool { return imgs[i].Rating > imgs[j].Rating }) + slices.SortFunc(imgs, func(a, b model.Image) bool { return a.Rating > b.Rating }) return imgs, nil } @@ -215,3 +219,14 @@ func (m *Mem) AlbumsToBeDeleted(_ context.Context) ([]model.Album, error) { } return albs, nil } + +func (m *Mem) Health(_ context.Context) (bool, error) { + return true, nil +} + +func (m *Mem) Reset() error { + m.syncAlbums.Lock() + defer m.syncAlbums.Unlock() + m.albums = map[uint64]model.Album{} + return nil +} diff --git a/infrastructure/database/mem_test.go b/infrastructure/database/mem_test.go index 2907700..5f81b77 100644 --- a/infrastructure/database/mem_test.go +++ b/infrastructure/database/mem_test.go @@ -1,290 +1,240 @@ -//go:build unit - package database import ( "context" - "reflect" "sort" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" - _ "github.com/zitryss/aye-and-nay/internal/config" + . "github.com/zitryss/aye-and-nay/internal/generator" . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" ) -func TestMemAlbum(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0x6CC4) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - edgs, err := mem.GetEdges(context.Background(), 0x6CC4) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(edgs, alb.Edges) { - t.Error("edgs != alb.GetEdges") - } +func TestMemTestSuite(t *testing.T) { + suite.Run(t, &MemTestSuite{}) +} + +type MemTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + db domain.Databaser + setupTestFn func() +} + +func (suite *MemTestSuite) SetupSuite() { + if !*unit { + suite.T().Skip() + } + ctx, cancel := context.WithCancel(context.Background()) + mem := NewMem(DefaultMemConfig) + suite.ctx = ctx + suite.cancel = cancel + suite.db = mem + suite.setupTestFn = suite.SetupTest +} + +func (suite *MemTestSuite) SetupTest() { + err := suite.db.(*Mem).Reset() + require.NoError(suite.T(), err) +} + +func (suite *MemTestSuite) TearDownTest() { + +} + +func (suite *MemTestSuite) TearDownSuite() { + err := suite.db.(*Mem).Reset() + require.NoError(suite.T(), err) + suite.cancel() +} + +func (suite *MemTestSuite) saveAlbum(id IdGenFunc, ids Ids) model.Album { + suite.T().Helper() + alb := AlbumFactory(id, ids) + err := suite.db.SaveAlbum(suite.ctx, alb) + assert.NoError(suite.T(), err) + return alb +} + +func (suite *MemTestSuite) TestAlbum() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + alb := suite.saveAlbum(id, ids) + edgs, err := suite.db.GetEdges(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, alb.Edges, edgs) }) - t.Run("Negative1", func(t *testing.T) { - mem := NewMem() - alb := AlbumFullFactory(0xA566) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - alb = AlbumFullFactory(0xA566) - err = mem.SaveAlbum(context.Background(), alb) - if !errors.Is(err, domain.ErrAlbumAlreadyExists) { - t.Error(err) - } + suite.T().Run("Negative1", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + alb := suite.saveAlbum(id, ids) + err := suite.db.SaveAlbum(suite.ctx, alb) + assert.ErrorIs(t, err, domain.ErrAlbumAlreadyExists) }) - t.Run("Negative2", func(t *testing.T) { - mem := NewMem() - _, err := mem.GetImagesIds(context.Background(), 0xA9B4) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative2", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + _, err := suite.db.GetImagesIds(suite.ctx, id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative3", func(t *testing.T) { - mem := NewMem() - _, err := mem.GetEdges(context.Background(), 0x3F1E) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative3", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + _, err := suite.db.GetEdges(suite.ctx, id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } -func TestMemCount(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0x746C) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - n, err := mem.CountImages(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - n, err = mem.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - err = mem.UpdateCompressionStatus(context.Background(), 0x746C, 0x3E3D) - if err != nil { - t.Error(err) - } - n, err = mem.CountImages(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - n, err = mem.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 1 { - t.Error("n != 1") - } - err = mem.UpdateCompressionStatus(context.Background(), 0x746C, 0xB399) - if err != nil { - t.Error(err) - } - n, err = mem.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 2 { - t.Error("n != 2") - } +func (suite *MemTestSuite) TestCount() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + n, err := suite.db.CountImages(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 5, n) + n, err = suite.db.CountImagesCompressed(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 0, n) + err = suite.db.UpdateCompressionStatus(suite.ctx, ids.Uint64(0), ids.Uint64(1)) + assert.NoError(t, err) + n, err = suite.db.CountImages(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 5, n) + n, err = suite.db.CountImagesCompressed(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 1, n) + err = suite.db.UpdateCompressionStatus(suite.ctx, ids.Uint64(0), ids.Uint64(2)) + assert.NoError(t, err) + n, err = suite.db.CountImagesCompressed(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 2, n) }) - t.Run("Negative1", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0x99DF) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mem.UpdateCompressionStatus(context.Background(), 0x99DF, 0x3E3D) - if err != nil { - t.Error(err) - } - err = mem.UpdateCompressionStatus(context.Background(), 0x99DF, 0x3E3D) - if err != nil { - t.Error(err) - } - n, err := mem.CountImagesCompressed(context.Background(), 0x99DF) - if err != nil { - t.Error(err) - } - if n != 1 { - t.Error("n != 1") - } + suite.T().Run("Negative1", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + err := suite.db.UpdateCompressionStatus(suite.ctx, ids.Uint64(0), ids.Uint64(1)) + assert.NoError(t, err) + err = suite.db.UpdateCompressionStatus(suite.ctx, ids.Uint64(0), ids.Uint64(1)) + assert.NoError(t, err) + n, err := suite.db.CountImagesCompressed(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 1, n) }) - t.Run("Negative2", func(t *testing.T) { - mem := NewMem() - _, err := mem.CountImages(context.Background(), 0xF256) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative2", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + _, err := suite.db.CountImages(suite.ctx, id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative3", func(t *testing.T) { - mem := NewMem() - _, err := mem.CountImagesCompressed(context.Background(), 0xC52A) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative3", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + _, err := suite.db.CountImagesCompressed(suite.ctx, id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative4", func(t *testing.T) { - mem := NewMem() - err := mem.UpdateCompressionStatus(context.Background(), 0xF73E, 0x3E3D) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative4", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + err := suite.db.UpdateCompressionStatus(suite.ctx, id(), ids.Uint64(0)) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative5", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0xDF75) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mem.UpdateCompressionStatus(context.Background(), 0xDF75, 0xE7A4) - if !errors.Is(err, domain.ErrImageNotFound) { - t.Error(err) - } + suite.T().Run("Negative5", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + err := suite.db.UpdateCompressionStatus(suite.ctx, ids.Uint64(0), id()) + assert.ErrorIs(t, err, domain.ErrImageNotFound) }) } -func TestMemImage(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0xB0C4) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - src, err := mem.GetImageSrc(context.Background(), 0xB0C4, 0x51DE) - if err != nil { - t.Error(err) - } - if src != "/aye-and-nay/albums/xLAAAAAAAAA/images/3lEAAAAAAAA" { - t.Error("src != \"/aye-and-nay/albums/xLAAAAAAAAA/images/3lEAAAAAAAA\"") - } +func (suite *MemTestSuite) TestImage() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + src, err := suite.db.GetImageSrc(suite.ctx, ids.Uint64(0), ids.Uint64(4)) + assert.NoError(t, err) + assert.Equal(t, "/aye-and-nay/albums/"+ids.Base64(0)+"/images/"+ids.Base64(4), src) }) - t.Run("Negative1", func(t *testing.T) { - mem := NewMem() - _, err := mem.GetImageSrc(context.Background(), 0x12EE, 0x51DE) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative1", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _, err := suite.db.GetImageSrc(suite.ctx, id(), ids.Uint64(0)) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative2", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0xD585) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - _, err = mem.GetImageSrc(context.Background(), 0xD585, 0xDA30) - if !errors.Is(err, domain.ErrImageNotFound) { - t.Error(err) - } + suite.T().Run("Negative2", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + _, err := suite.db.GetImageSrc(suite.ctx, ids.Uint64(0), id()) + assert.ErrorIs(t, err, domain.ErrImageNotFound) }) } -func TestMemVote(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - alb := AlbumFullFactory(0x4D76) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mem.SaveVote(context.Background(), 0x4D76, 0xDA2A, 0xDA52) - if err != nil { - t.Error(err) - } - err = mem.SaveVote(context.Background(), 0x4D76, 0xDA2A, 0xDA52) - if err != nil { - t.Error(err) - } - edgs, err := mem.GetEdges(context.Background(), 0x4D76) - if err != nil { - t.Error(err) - } - if edgs[0xDA2A][0xDA52] != 2 { - t.Error("edgs[imageFrom][imageTo] != 2") - } +func (suite *MemTestSuite) TestVote() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + err := suite.db.SaveVote(suite.ctx, ids.Uint64(0), ids.Uint64(3), ids.Uint64(5)) + assert.NoError(t, err) + err = suite.db.SaveVote(suite.ctx, ids.Uint64(0), ids.Uint64(3), ids.Uint64(5)) + assert.NoError(t, err) + edgs, err := suite.db.GetEdges(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 2, edgs[ids.Uint64(3)][ids.Uint64(5)]) }) - t.Run("Negative", func(t *testing.T) { - mem := NewMem() - err := mem.SaveVote(context.Background(), 0x1FAD, 0x84E6, 0x308E) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + err := suite.db.SaveVote(suite.ctx, id(), id(), id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } -func TestMemSort(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - alb := AlbumFullFactory(0x5A96) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - imgs1, err := mem.GetImagesOrdered(context.Background(), 0x5A96) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/3lEAAAAAAAA", Rating: 0.77920413} - img2 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/PT4AAAAAAAA", Rating: 0.48954984} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/KtoAAAAAAAA", Rating: 0.41218211} - img4 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/mbMAAAAAAAA", Rating: 0.19186324} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/UtoAAAAAAAA", Rating: 0.13278389} +func (suite *MemTestSuite) TestSort() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + imgs1, err := suite.db.GetImagesOrdered(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + img1 := model.Image{Id: ids.Uint64(4), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(4), Rating: 0.77920413} + img2 := model.Image{Id: ids.Uint64(1), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(1), Rating: 0.48954984} + img3 := model.Image{Id: ids.Uint64(3), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(3), Rating: 0.41218211} + img4 := model.Image{Id: ids.Uint64(2), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(2), Rating: 0.19186324} + img5 := model.Image{Id: ids.Uint64(5), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(5), Rating: 0.13278389} imgs2 := []model.Image{img1, img2, img3, img4, img5} - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } + assert.Equal(t, imgs2, imgs1) }) - t.Run("Negative", func(t *testing.T) { - mem := NewMem() - _, err := mem.GetImagesOrdered(context.Background(), 0x66BE) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + id, _ := GenId() + _, err := suite.db.GetImagesOrdered(suite.ctx, id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } -func TestMemRatings(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mem := NewMem() - alb := AlbumFullFactory(0x4E54) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/PT4AAAAAAAA", Rating: 0.54412788} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/mbMAAAAAAAA", Rating: 0.32537162} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/KtoAAAAAAAA", Rating: 0.43185491} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/3lEAAAAAAAA", Rating: 0.57356209} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/UtoAAAAAAAA", Rating: 0.61438023} +func (suite *MemTestSuite) TestRatings() { + suite.T().Run("Positive", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + img1 := model.Image{Id: ids.Uint64(1), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(1), Rating: 0.54412788} + img2 := model.Image{Id: ids.Uint64(2), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(2), Rating: 0.32537162} + img3 := model.Image{Id: ids.Uint64(3), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(3), Rating: 0.43185491} + img4 := model.Image{Id: ids.Uint64(4), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(4), Rating: 0.57356209} + img5 := model.Image{Id: ids.Uint64(5), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(5), Rating: 0.61438023} imgs1 := []model.Image{img1, img2, img3, img4, img5} vector := map[uint64]float64{} vector[img1.Id] = img1.Rating @@ -292,108 +242,73 @@ func TestMemRatings(t *testing.T) { vector[img3.Id] = img3.Rating vector[img4.Id] = img4.Rating vector[img5.Id] = img5.Rating - err = mem.UpdateRatings(context.Background(), 0x4E54, vector) - if err != nil { - t.Error(err) - } - imgs2, err := mem.GetImagesOrdered(context.Background(), 0x4E54) - if err != nil { - t.Error(err) - } + err := suite.db.UpdateRatings(suite.ctx, ids.Uint64(0), vector) + assert.NoError(t, err) + imgs2, err := suite.db.GetImagesOrdered(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) sort.Slice(imgs1, func(i, j int) bool { return imgs1[i].Rating > imgs1[j].Rating }) - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } + assert.Equal(t, imgs1, imgs2) }) - t.Run("Negative", func(t *testing.T) { - mem := NewMem() - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/PT4AAAAAAAA", Rating: 0.54412788} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/mbMAAAAAAAA", Rating: 0.32537162} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/KtoAAAAAAAA", Rating: 0.43185491} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/3lEAAAAAAAA", Rating: 0.57356209} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/UtoAAAAAAAA", Rating: 0.61438023} + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + album := id() + img1 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(1), Rating: 0.54412788} + img2 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(2), Rating: 0.32537162} + img3 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(3), Rating: 0.43185491} + img4 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(4), Rating: 0.57356209} + img5 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(5), Rating: 0.61438023} vector := map[uint64]float64{} vector[img1.Id] = img1.Rating vector[img2.Id] = img2.Rating vector[img3.Id] = img3.Rating vector[img4.Id] = img4.Rating vector[img5.Id] = img5.Rating - err := mem.UpdateRatings(context.Background(), 0xA293, vector) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + err := suite.db.UpdateRatings(suite.ctx, album, vector) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } -func TestMemDelete(t *testing.T) { - t.Run("Positive1", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0x748C) - _, err := mem.CountImages(context.Background(), 0x748C) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - err = mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - n, err := mem.CountImages(context.Background(), 0x748C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - albums, err := mem.AlbumsToBeDeleted(context.Background()) - if err != nil { - t.Error(err) - } - if albums != nil { - t.Error("albums != nil") - } - err = mem.DeleteAlbum(context.Background(), 0x748C) - if err != nil { - t.Error(err) - } - _, err = mem.CountImages(context.Background(), 0x748C) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } +func (suite *MemTestSuite) TestDelete() { + suite.T().Run("Positive1", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + alb := AlbumFactory(id, ids) + _, err := suite.db.CountImages(suite.ctx, ids.Uint64(0)) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) + err = suite.db.SaveAlbum(suite.ctx, alb) + assert.NoError(t, err) + n, err := suite.db.CountImages(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + assert.Equal(t, 5, n) + albums, err := suite.db.AlbumsToBeDeleted(suite.ctx) + assert.NoError(t, err) + assert.Len(t, albums, 0) + err = suite.db.DeleteAlbum(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + _, err = suite.db.CountImages(suite.ctx, ids.Uint64(0)) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Positive2", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0x7B43) + suite.T().Run("Positive2", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + alb := AlbumFactory(id, ids) alb.Expires = time.Now().Add(-1 * time.Hour) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - albums, err := mem.AlbumsToBeDeleted(context.Background()) - if err != nil { - t.Error(err) - } - if !(len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero()) { - t.Error("!(len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero())") - } - err = mem.DeleteAlbum(context.Background(), 0x7B43) - if err != nil { - t.Error(err) - } - _, err = mem.CountImages(context.Background(), 0x7B43) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + err := suite.db.SaveAlbum(suite.ctx, alb) + assert.NoError(t, err) + albums, err := suite.db.AlbumsToBeDeleted(suite.ctx) + assert.NoError(t, err) + assert.True(t, len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero()) + err = suite.db.DeleteAlbum(suite.ctx, ids.Uint64(0)) + assert.NoError(t, err) + _, err = suite.db.CountImages(suite.ctx, ids.Uint64(0)) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) - t.Run("Negative", func(t *testing.T) { - mem := NewMem() - alb := AlbumEmptyFactory(0x608C) - err := mem.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mem.DeleteAlbum(context.Background(), 0xB7FF) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } + suite.T().Run("Negative", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + _ = suite.saveAlbum(id, ids) + err := suite.db.DeleteAlbum(suite.ctx, id()) + assert.ErrorIs(t, err, domain.ErrAlbumNotFound) }) } diff --git a/infrastructure/database/mongo.go b/infrastructure/database/mongo.go index 634ff75..843ae9c 100644 --- a/infrastructure/database/mongo.go +++ b/infrastructure/database/mongo.go @@ -17,6 +17,10 @@ import ( "github.com/zitryss/aye-and-nay/pkg/retry" ) +var ( + _ domain.Databaser = (*Mongo)(nil) +) + type albumLru map[uint64]string type imageDao struct { @@ -35,17 +39,24 @@ type edgeDao struct { Weight int } -func NewMongo() (*Mongo, error) { - conf := newMongoConfig() - ctx, cancel := context.WithTimeout(context.Background(), conf.timeout) +func NewMongo(ctx context.Context, conf MongoConfig) (*Mongo, error) { + ctx, cancel := context.WithTimeout(ctx, conf.Timeout) defer cancel() - opts := optionsdb.Client().ApplyURI("mongodb://" + conf.host + ":" + conf.port) + opts := optionsdb.Client().ApplyURI("mongodb://" + conf.Host + ":" + conf.Port) client, err := mongodb.Connect(ctx, opts) if err != nil { return &Mongo{}, errors.Wrap(err) } - err = retry.Do(conf.times, conf.pause, func() error { - err := client.Ping(ctx, readpref.Primary()) + db := client.Database("aye-and-nay") + images := db.Collection("images") + edges := db.Collection("edges") + cache, err := lru.New(conf.LRU) + if err != nil { + return &Mongo{}, errors.Wrap(err) + } + m := &Mongo{conf, client, db, images, edges, cache} + err = retry.Do(conf.RetryTimes, conf.RetryPause, func() error { + _, err := m.Health(ctx) if err != nil { return errors.Wrap(err) } @@ -54,18 +65,11 @@ func NewMongo() (*Mongo, error) { if err != nil { return &Mongo{}, errors.Wrap(err) } - db := client.Database("aye-and-nay") - images := db.Collection("images") - edges := db.Collection("edges") - cache, err := lru.New(conf.lru) - if err != nil { - return &Mongo{}, errors.Wrap(err) - } - return &Mongo{conf, client, db, images, edges, cache}, nil + return m, nil } type Mongo struct { - conf mongoConfig + conf MongoConfig client *mongodb.Client db *mongodb.Database images *mongodb.Collection @@ -82,10 +86,10 @@ func (m *Mongo) SaveAlbum(ctx context.Context, alb model.Album) error { if n > 0 { return errors.Wrap(domain.ErrAlbumAlreadyExists) } - imgsDao := make([]interface{}, 0, len(alb.Images)) + imgsDao := make([]any, 0, len(alb.Images)) albLru := make(albumLru, len(alb.Images)) for _, img := range alb.Images { - imgDao := imageDao{int64(alb.Id), int64(img.Id), img.Src, img.Rating, m.conf.compressed, alb.Expires} + imgDao := imageDao{int64(alb.Id), int64(img.Id), img.Src, img.Rating, m.conf.Compressed, alb.Expires} imgsDao = append(imgsDao, imgDao) albLru[img.Id] = img.Src } @@ -341,8 +345,16 @@ func (m *Mongo) lruAdd(ctx context.Context, album uint64) error { return nil } -func (m *Mongo) Close() error { - ctx, cancel := context.WithTimeout(context.Background(), m.conf.timeout) +func (m *Mongo) Health(ctx context.Context) (bool, error) { + err := m.client.Ping(ctx, readpref.Primary()) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthDatabase, "%s", err) + } + return true, err +} + +func (m *Mongo) Close(ctx context.Context) error { + ctx, cancel := context.WithTimeout(ctx, m.conf.Timeout) defer cancel() err := m.client.Disconnect(ctx) if err != nil { @@ -350,3 +362,12 @@ func (m *Mongo) Close() error { } return nil } + +func (m *Mongo) Reset() error { + err := m.db.Drop(context.Background()) + if err != nil { + return errors.Wrap(err) + } + m.cache.Purge() + return nil +} diff --git a/infrastructure/database/mongo_integration_test.go b/infrastructure/database/mongo_integration_test.go index b757411..8dc741e 100644 --- a/infrastructure/database/mongo_integration_test.go +++ b/infrastructure/database/mongo_integration_test.go @@ -1,492 +1,90 @@ -//go:build integration - package database import ( "context" - "reflect" - "sort" "testing" - "time" - "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/domain/model" - _ "github.com/zitryss/aye-and-nay/internal/config" - . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + . "github.com/zitryss/aye-and-nay/internal/generator" ) -func TestMongoAlbum(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x6CC4) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - edgs, err := mongo.GetEdges(context.Background(), 0x6CC4) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(edgs, alb.Edges) { - t.Error("edgs != alb.GetEdges") - } - }) - t.Run("Negative1", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0xA566) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - alb = AlbumFullFactory(0xA566) - err = mongo.SaveAlbum(context.Background(), alb) - if !errors.Is(err, domain.ErrAlbumAlreadyExists) { - t.Error(err) - } - }) - t.Run("Negative2", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - _, err = mongo.GetImagesIds(context.Background(), 0xA9B4) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative3", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - _, err = mongo.GetEdges(context.Background(), 0x3F1E) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func TestMongoTestSuite(t *testing.T) { + suite.Run(t, &MongoTestSuite{}) } -func TestMongoCount(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x746C) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - n, err := mongo.CountImages(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - n, err = mongo.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 0 { - t.Error("n != 0") - } - err = mongo.UpdateCompressionStatus(context.Background(), 0x746C, 0x3E3D) - if err != nil { - t.Error(err) - } - n, err = mongo.CountImages(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - n, err = mongo.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 1 { - t.Error("n != 1") - } - err = mongo.UpdateCompressionStatus(context.Background(), 0x746C, 0xB399) - if err != nil { - t.Error(err) - } - n, err = mongo.CountImagesCompressed(context.Background(), 0x746C) - if err != nil { - t.Error(err) - } - if n != 2 { - t.Error("n != 2") - } - }) - t.Run("Negative1", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x99DF) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mongo.UpdateCompressionStatus(context.Background(), 0x99DF, 0x3E3D) - if err != nil { - t.Error(err) - } - err = mongo.UpdateCompressionStatus(context.Background(), 0x99DF, 0x3E3D) - if err != nil { - t.Error(err) - } - n, err := mongo.CountImagesCompressed(context.Background(), 0x99DF) - if err != nil { - t.Error(err) - } - if n != 1 { - t.Error("n != 1") - } - }) - t.Run("Negative2", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - _, err = mongo.CountImages(context.Background(), 0xF256) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative3", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - _, err = mongo.CountImagesCompressed(context.Background(), 0xC52A) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative4", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - err = mongo.UpdateCompressionStatus(context.Background(), 0xF73E, 0x3E3D) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative5", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0xDF75) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mongo.UpdateCompressionStatus(context.Background(), 0xDF75, 0xE7A4) - if !errors.Is(err, domain.ErrImageNotFound) { - t.Error(err) - } - }) +type MongoTestSuite struct { + suite.Suite + base MemTestSuite } -func TestMongoImage(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0xB0C4) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - src, err := mongo.GetImageSrc(context.Background(), 0xB0C4, 0x51DE) - if err != nil { - t.Error(err) - } - if src != "/aye-and-nay/albums/xLAAAAAAAAA/images/3lEAAAAAAAA" { - t.Error("src != \"/aye-and-nay/albums/xLAAAAAAAAA/images/3lEAAAAAAAA\"") - } - }) - t.Run("Negative1", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - _, err = mongo.GetImageSrc(context.Background(), 0x12EE, 0x51DE) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Negative2", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0xD585) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - _, err = mongo.GetImageSrc(context.Background(), 0xD585, 0xDA30) - if !errors.Is(err, domain.ErrImageNotFound) { - t.Error(err) - } - }) +func (suite *MongoTestSuite) SetupSuite() { + if !*integration { + suite.T().Skip() + } + suite.base = MemTestSuite{} + suite.base.SetT(suite.T()) + ctx, cancel := context.WithCancel(context.Background()) + mongo, err := NewMongo(ctx, DefaultMongoConfig) + require.NoError(suite.T(), err) + suite.base.ctx = ctx + suite.base.cancel = cancel + suite.base.db = mongo + suite.base.setupTestFn = suite.SetupTest } -func TestMongoVote(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0x4D76) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mongo.SaveVote(context.Background(), 0x4D76, 0xDA2A, 0xDA52) - if err != nil { - t.Error(err) - } - err = mongo.SaveVote(context.Background(), 0x4D76, 0xDA2A, 0xDA52) - if err != nil { - t.Error(err) - } - edgs, err := mongo.GetEdges(context.Background(), 0x4D76) - if err != nil { - t.Error(err) - } - if edgs[0xDA2A][0xDA52] != 2 { - t.Error("edgs[imageFrom][imageTo] != 2") - } - }) - t.Run("Negative", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - err = mongo.SaveVote(context.Background(), 0x1FAD, 0x84E6, 0x308E) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func (suite *MongoTestSuite) SetupTest() { + err := suite.base.db.(*Mongo).Reset() + require.NoError(suite.T(), err) } -func TestMongoSort(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0x5A96) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - imgs1, err := mongo.GetImagesOrdered(context.Background(), 0x5A96) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/3lEAAAAAAAA", Rating: 0.77920413} - img2 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/PT4AAAAAAAA", Rating: 0.48954984} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/KtoAAAAAAAA", Rating: 0.41218211} - img4 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/mbMAAAAAAAA", Rating: 0.19186324} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/lloAAAAAAAA/images/UtoAAAAAAAA", Rating: 0.13278389} - imgs2 := []model.Image{img1, img2, img3, img4, img5} - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } - }) - t.Run("Negative", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - _, err = mongo.GetImagesOrdered(context.Background(), 0x66BE) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func (suite *MongoTestSuite) TearDownTest() { + } -func TestMongoRatings(t *testing.T) { - t.Run("Positive", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumFullFactory(0x4E54) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/PT4AAAAAAAA", Rating: 0.54412788} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/mbMAAAAAAAA", Rating: 0.32537162} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/KtoAAAAAAAA", Rating: 0.43185491} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/3lEAAAAAAAA", Rating: 0.57356209} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/VE4AAAAAAAA/images/UtoAAAAAAAA", Rating: 0.61438023} - imgs1 := []model.Image{img1, img2, img3, img4, img5} - vector := map[uint64]float64{} - vector[img1.Id] = img1.Rating - vector[img2.Id] = img2.Rating - vector[img3.Id] = img3.Rating - vector[img4.Id] = img4.Rating - vector[img5.Id] = img5.Rating - err = mongo.UpdateRatings(context.Background(), 0x4E54, vector) - if err != nil { - t.Error(err) - } - imgs2, err := mongo.GetImagesOrdered(context.Background(), 0x4E54) - if err != nil { - t.Error(err) - } - sort.Slice(imgs1, func(i, j int) bool { return imgs1[i].Rating > imgs1[j].Rating }) - if !reflect.DeepEqual(imgs1, imgs2) { - t.Error("imgs1 != imgs2") - } - }) - t.Run("Negative", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/PT4AAAAAAAA", Rating: 0.54412788} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/mbMAAAAAAAA", Rating: 0.32537162} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/KtoAAAAAAAA", Rating: 0.43185491} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/3lEAAAAAAAA", Rating: 0.57356209} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/k6IAAAAAAAA/images/UtoAAAAAAAA", Rating: 0.61438023} - vector := map[uint64]float64{} - vector[img1.Id] = img1.Rating - vector[img2.Id] = img2.Rating - vector[img3.Id] = img3.Rating - vector[img4.Id] = img4.Rating - vector[img5.Id] = img5.Rating - err = mongo.UpdateRatings(context.Background(), 0xA293, vector) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func (suite *MongoTestSuite) TearDownSuite() { + err := suite.base.db.(*Mongo).Reset() + require.NoError(suite.T(), err) + err = suite.base.db.(*Mongo).Close(suite.base.ctx) + require.NoError(suite.T(), err) + suite.base.cancel() } -func TestMongoDelete(t *testing.T) { - t.Run("Positive1", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x748C) - _, err = mongo.CountImages(context.Background(), 0x748C) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - n, err := mongo.CountImages(context.Background(), 0x748C) - if err != nil { - t.Error(err) - } - if n != 5 { - t.Error("n != 5") - } - albums, err := mongo.AlbumsToBeDeleted(context.Background()) - if err != nil { - t.Error(err) - } - if len(albums) != 0 { - t.Error("len(albums) != 0") - } - err = mongo.DeleteAlbum(context.Background(), 0x748C) - if err != nil { - t.Error(err) - } - _, err = mongo.CountImages(context.Background(), 0x748C) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) - t.Run("Positive2", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x7B43) - alb.Expires = time.Now().Add(-1 * time.Hour) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - albums, err := mongo.AlbumsToBeDeleted(context.Background()) - if err != nil { - t.Error(err) - } - if !(len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero()) { - t.Error("!(len(albums) == 1 && albums[0].Id == alb.Id && !albums[0].Expires.IsZero())") - } - err = mongo.DeleteAlbum(context.Background(), 0x7B43) - if err != nil { - t.Error(err) - } - _, err = mongo.CountImages(context.Background(), 0x7B43) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - t.Cleanup(func() { - _ = mongo.DeleteAlbum(context.Background(), 0x7B43) - }) - }) - t.Run("Negative", func(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb := AlbumEmptyFactory(0x608C) - err = mongo.SaveAlbum(context.Background(), alb) - if err != nil { - t.Error(err) - } - err = mongo.DeleteAlbum(context.Background(), 0xB7FF) - if !errors.Is(err, domain.ErrAlbumNotFound) { - t.Error(err) - } - }) +func (suite *MongoTestSuite) TestMongoAlbum() { + suite.base.TestAlbum() } -func TestMongoLru(t *testing.T) { - mongo, err := NewMongo() - if err != nil { - t.Fatal(err) - } - alb1 := AlbumEmptyFactory(0x36FC) - err = mongo.SaveAlbum(context.Background(), alb1) - if err != nil { - t.Error(err) - } - alb2 := AlbumEmptyFactory(0xB020) - err = mongo.SaveAlbum(context.Background(), alb2) - if err != nil { - t.Error(err) - } - edgs, err := mongo.GetEdges(context.Background(), 0x36FC) - if err != nil { - t.Error(err) - } - if !reflect.DeepEqual(edgs, alb1.Edges) { - t.Error("edgs != alb1.GetEdges") - } +func (suite *MongoTestSuite) TestMongoCount() { + suite.base.TestCount() +} + +func (suite *MongoTestSuite) TestMongoImage() { + suite.base.TestImage() +} + +func (suite *MongoTestSuite) TestMongoVote() { + suite.base.TestVote() +} + +func (suite *MongoTestSuite) TestMongoSort() { + suite.base.TestSort() +} + +func (suite *MongoTestSuite) TestMongoRatings() { + suite.base.TestRatings() +} + +func (suite *MongoTestSuite) TestMongoDelete() { + suite.base.TestDelete() +} + +func (suite *MongoTestSuite) TestMongoLru() { + id, ids := GenId() + alb1 := suite.base.saveAlbum(id, ids) + _ = suite.base.saveAlbum(id, ids) + edgs, err := suite.base.db.GetEdges(suite.base.ctx, ids.Uint64(0)) + assert.NoError(suite.base.T(), err) + assert.Equal(suite.base.T(), alb1.Edges, edgs) } diff --git a/infrastructure/storage/config.go b/infrastructure/storage/config.go index c0f6d45..2ca2150 100644 --- a/infrastructure/storage/config.go +++ b/infrastructure/storage/config.go @@ -2,36 +2,39 @@ package storage import ( "time" - - "github.com/spf13/viper" ) -func newMinioConfig() minioConfig { - return minioConfig{ - host: viper.GetString("storage.minio.host"), - port: viper.GetString("storage.minio.port"), - accessKey: viper.GetString("storage.minio.accessKey"), - secretKey: viper.GetString("storage.minio.secretKey"), - token: viper.GetString("storage.minio.token"), - secure: viper.GetBool("storage.minio.secure"), - times: viper.GetInt("storage.minio.retry.times"), - pause: viper.GetDuration("storage.minio.retry.pause"), - timeout: viper.GetDuration("storage.minio.retry.timeout"), - location: viper.GetString("storage.minio.location"), - prefix: viper.GetString("storage.minio.prefix"), - } +type StorageConfig struct { + Storage string `mapstructure:"APP_STORAGE" validate:"required"` + Minio MinioConfig `mapstructure:",squash"` } -type minioConfig struct { - host string - port string - accessKey string - secretKey string - token string - secure bool - times int - pause time.Duration - timeout time.Duration - location string - prefix string +type MinioConfig struct { + Host string `mapstructure:"STORAGE_MINIO_HOST" validate:"required"` + Port string `mapstructure:"STORAGE_MINIO_PORT" validate:"required"` + AccessKey string `mapstructure:"STORAGE_MINIO_ACCESS_KEY" validate:"required"` + SecretKey string `mapstructure:"STORAGE_MINIO_SECRET_KEY" validate:"required"` + Token string `mapstructure:"STORAGE_MINIO_TOKEN"` + Secure bool `mapstructure:"STORAGE_MINIO_SECURE"` + RetryTimes int `mapstructure:"STORAGE_MINIO_RETRY_TIMES" validate:"required"` + RetryPause time.Duration `mapstructure:"STORAGE_MINIO_RETRY_PAUSE" validate:"required"` + Timeout time.Duration `mapstructure:"STORAGE_MINIO_TIMEOUT" validate:"required"` + Location string `mapstructure:"STORAGE_MINIO_LOCATION" validate:"required"` + Prefix string `mapstructure:"STORAGE_MINIO_PREFIX"` } + +var ( + DefaultMinioConfig = MinioConfig{ + Host: "localhost", + Port: "9000", + AccessKey: "12345678", + SecretKey: "qwertyui", + Token: "", + Secure: false, + RetryTimes: 4, + RetryPause: 5 * time.Second, + Timeout: 30 * time.Second, + Location: "eu-central-1", + Prefix: "", + } +) diff --git a/infrastructure/storage/minio.go b/infrastructure/storage/minio.go index 80ecce7..2a5bc48 100644 --- a/infrastructure/storage/minio.go +++ b/infrastructure/storage/minio.go @@ -1,15 +1,11 @@ package storage import ( - "bufio" - "bytes" "context" "io" - "mime/multipart" "net/http" - "os" - minios3 "github.com/minio/minio-go/v7" + minioS3 "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/zitryss/aye-and-nay/domain/domain" @@ -20,65 +16,23 @@ import ( "github.com/zitryss/aye-and-nay/pkg/retry" ) -func NewMinio() (*Minio, error) { - conf := newMinioConfig() - client, err := minios3.New(conf.host+":"+conf.port, &minios3.Options{ - Creds: credentials.NewStaticV4(conf.accessKey, conf.secretKey, conf.token), - Secure: conf.secure, +var ( + _ domain.Storager = (*Minio)(nil) +) + +func NewMinio(ctx context.Context, conf MinioConfig) (*Minio, error) { + client, err := minioS3.New(conf.Host+":"+conf.Port, &minioS3.Options{ + Creds: credentials.NewStaticV4(conf.AccessKey, conf.SecretKey, conf.Token), + Secure: conf.Secure, }) if err != nil { return &Minio{}, errors.Wrap(err) } - ctx, cancel := context.WithTimeout(context.Background(), conf.timeout) + m := &Minio{conf, client} + ctx, cancel := context.WithTimeout(ctx, conf.Timeout) defer cancel() - err = retry.Do(conf.times, conf.pause, func() error { - c := http.Client{} - url := "http://" + conf.host + ":" + conf.port + "/minio/health/live" - body := io.Reader(nil) - req, err := http.NewRequestWithContext(ctx, "GET", url, body) - if err != nil { - return errors.Wrap(err) - } - resp, err := c.Do(req) - if err != nil { - return errors.Wrap(err) - } - _, err = io.Copy(io.Discard, resp.Body) - if err != nil { - _ = resp.Body.Close() - return errors.Wrap(err) - } - err = resp.Body.Close() - if err != nil { - return errors.Wrap(err) - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return errors.Wrap(errors.New("no connection to minio")) - } - c = http.Client{} - url = "http://" + conf.host + ":" + conf.port + "/minio/health/ready" - body = io.Reader(nil) - req, err = http.NewRequestWithContext(ctx, "GET", url, body) - if err != nil { - return errors.Wrap(err) - } - resp, err = c.Do(req) - if err != nil { - return errors.Wrap(err) - } - _, err = io.Copy(io.Discard, resp.Body) - if err != nil { - _ = resp.Body.Close() - return errors.Wrap(err) - } - err = resp.Body.Close() - if err != nil { - return errors.Wrap(err) - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return errors.Wrap(errors.New("minio is not ready")) - } - _, err = client.ListBuckets(ctx) + err = retry.Do(conf.RetryTimes, conf.RetryPause, func() error { + _, err := m.Health(ctx) if err != nil { return errors.Wrap(err) } @@ -92,7 +46,7 @@ func NewMinio() (*Minio, error) { return &Minio{}, errors.Wrap(err) } if !found { - err = client.MakeBucket(ctx, "aye-and-nay", minios3.MakeBucketOptions{Region: conf.location}) + err = client.MakeBucket(ctx, "aye-and-nay", minioS3.MakeBucketOptions{Region: conf.Location}) if err != nil { return &Minio{}, errors.Wrap(err) } @@ -102,37 +56,24 @@ func NewMinio() (*Minio, error) { return &Minio{}, errors.Wrap(err) } } - return &Minio{conf, client}, nil + return m, nil } type Minio struct { - conf minioConfig - client *minios3.Client + conf MinioConfig + client *minioS3.Client } func (m *Minio) Put(ctx context.Context, album uint64, image uint64, f model.File) (string, error) { - defer func() { - switch v := f.Reader.(type) { - case *os.File: - _ = v.Close() - _ = os.Remove(v.Name()) - case multipart.File: - _ = v.Close() - case *bytes.Buffer: - pool.PutBuffer(v) - default: - panic(errors.Wrap(domain.ErrUnknown)) - } - }() + defer f.Close() albumB64 := base64.FromUint64(album) imageB64 := base64.FromUint64(image) filename := "albums/" + albumB64 + "/images/" + imageB64 - buf := bufio.NewReader(f) - _, err := m.client.PutObject(ctx, "aye-and-nay", filename, buf, f.Size, minios3.PutObjectOptions{}) + _, err := m.client.PutObject(ctx, "aye-and-nay", filename, f.Reader, f.Size, minioS3.PutObjectOptions{}) if err != nil { return "", errors.Wrap(err) } - src := m.conf.prefix + "/aye-and-nay/" + filename + src := m.conf.Prefix + "/aye-and-nay/" + filename return src, nil } @@ -140,23 +81,110 @@ func (m *Minio) Get(ctx context.Context, album uint64, image uint64) (model.File albumB64 := base64.FromUint64(album) imageB64 := base64.FromUint64(image) filename := "albums/" + albumB64 + "/images/" + imageB64 - obj, err := m.client.GetObject(ctx, "aye-and-nay", filename, minios3.GetObjectOptions{}) + obj, err := m.client.GetObject(ctx, "aye-and-nay", filename, minioS3.GetObjectOptions{}) + if err != nil { + return model.File{}, errors.Wrap(err) + } + defer obj.Close() + info, err := obj.Stat() if err != nil { return model.File{}, errors.Wrap(err) } - buf := pool.GetBuffer() + buf := pool.GetBufferN(info.Size) n, err := io.Copy(buf, obj) if err != nil { return model.File{}, errors.Wrap(err) } - return model.File{Reader: buf, Size: n}, nil + closeFn := func() error { + pool.PutBuffer(buf) + return nil + } + return model.NewFile(buf, closeFn, n), nil } func (m *Minio) Remove(ctx context.Context, album uint64, image uint64) error { albumB64 := base64.FromUint64(album) imageB64 := base64.FromUint64(image) filename := "albums/" + albumB64 + "/images/" + imageB64 - err := m.client.RemoveObject(ctx, "aye-and-nay", filename, minios3.RemoveObjectOptions{}) + err := m.client.RemoveObject(ctx, "aye-and-nay", filename, minioS3.RemoveObjectOptions{}) + if err != nil { + return errors.Wrap(err) + } + return nil +} + +func (m *Minio) Health(ctx context.Context) (bool, error) { + url := "http://" + m.conf.Host + ":" + m.conf.Port + "/minio/health/live" + body := io.Reader(nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, body) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + c := http.Client{Timeout: m.conf.Timeout} + resp, err := c.Do(req) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + _ = resp.Body.Close() + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + err = resp.Body.Close() + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + if resp.StatusCode/100 != 2 { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", "no connection to minio") + } + url = "http://" + m.conf.Host + ":" + m.conf.Port + "/minio/health/ready" + body = io.Reader(nil) + req, err = http.NewRequestWithContext(ctx, http.MethodGet, url, body) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + c = http.Client{Timeout: m.conf.Timeout} + resp, err = c.Do(req) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + _ = resp.Body.Close() + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + err = resp.Body.Close() + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + if resp.StatusCode/100 != 2 { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", "minio is not ready") + } + _, err = m.client.ListBuckets(ctx) + if err != nil { + return false, errors.Wrapf(domain.ErrBadHealthStorage, "%s", err) + } + return true, nil +} + +func (m *Minio) Reset() error { + bb, err := m.client.ListBuckets(context.Background()) + if err != nil { + return errors.Wrap(err) + } + for _, b := range bb { + for obj := range m.client.ListObjects(context.Background(), b.Name, minioS3.ListObjectsOptions{Recursive: true}) { + err := m.client.RemoveObject(context.Background(), b.Name, obj.Key, minioS3.RemoveObjectOptions{ForceDelete: true}) + if err != nil { + return errors.Wrap(err) + } + } + err := m.client.RemoveBucket(context.Background(), b.Name) + if err != nil { + return errors.Wrap(err) + } + } + err = m.client.MakeBucket(context.Background(), "aye-and-nay", minioS3.MakeBucketOptions{Region: m.conf.Location}) if err != nil { return errors.Wrap(err) } diff --git a/infrastructure/storage/minio_integration_test.go b/infrastructure/storage/minio_integration_test.go index 15ff9a9..bc9de09 100644 --- a/infrastructure/storage/minio_integration_test.go +++ b/infrastructure/storage/minio_integration_test.go @@ -1,88 +1,102 @@ -//go:build integration - package storage import ( "context" "testing" - minios3 "github.com/minio/minio-go/v7" + minioS3 "github.com/minio/minio-go/v7" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" - _ "github.com/zitryss/aye-and-nay/internal/config" + "github.com/zitryss/aye-and-nay/domain/domain" + . "github.com/zitryss/aye-and-nay/internal/generator" . "github.com/zitryss/aye-and-nay/internal/testing" - "github.com/zitryss/aye-and-nay/pkg/errors" ) -func TestMinio(t *testing.T) { - t.Run("", func(t *testing.T) { - minio, err := NewMinio() - if err != nil { - t.Fatal(err) - } - f, err := minio.Get(context.Background(), 0x70D8, 0xD5C7) - e := (*minios3.ErrorResponse)(nil) - if errors.As(err, &e) { - t.Error(err) - } - if f.Reader != nil { - t.Error("f.Reader != nil") - } - src, err := minio.Put(context.Background(), 0x70D8, 0xD5C7, Png()) - if err != nil { - t.Error(err) - } - if src != "/aye-and-nay/albums/2HAAAAAAAAA/images/x9UAAAAAAAA" { - t.Error("src != \"/aye-and-nay/albums/2HAAAAAAAAA/images/x9UAAAAAAAA\"") - } - f, err = minio.Get(context.Background(), 0x70D8, 0xD5C7) - if err != nil { - t.Error(err) - } - if !EqualFile(f, Png()) { - t.Error("!EqualFile(f, Png())") - } - err = minio.Remove(context.Background(), 0x70D8, 0xD5C7) - if err != nil { - t.Error(err) - } - f, err = minio.Get(context.Background(), 0x70D8, 0xD5C7) - e = (*minios3.ErrorResponse)(nil) - if errors.As(err, &e) { - t.Error(err) - } - if f.Reader != nil { - t.Error("f.Reader != nil") - } +func TestMinioTestSuite(t *testing.T) { + suite.Run(t, &MinioTestSuite{}) +} + +type MinioTestSuite struct { + suite.Suite + ctx context.Context + cancel context.CancelFunc + storage domain.Storager + setupTestFn func() +} + +func (suite *MinioTestSuite) SetupSuite() { + if !*integration { + suite.T().Skip() + } + ctx, cancel := context.WithCancel(context.Background()) + minio, err := NewMinio(ctx, DefaultMinioConfig) + require.NoError(suite.T(), err) + suite.ctx = ctx + suite.cancel = cancel + suite.storage = minio + suite.setupTestFn = suite.SetupTest +} + +func (suite *MinioTestSuite) SetupTest() { + err := suite.storage.(*Minio).Reset() + require.NoError(suite.T(), err) +} + +func (suite *MinioTestSuite) TearDownTest() { + +} + +func (suite *MinioTestSuite) TearDownSuite() { + err := suite.storage.(*Minio).Reset() + require.NoError(suite.T(), err) + suite.cancel() +} + +func (suite *MinioTestSuite) TestMinio() { + suite.T().Run("", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + album := id() + image := id() + f, err := suite.storage.Get(suite.ctx, album, image) + e := minioS3.ErrorResponse{} + assert.ErrorAs(t, err, &e) + assert.Nil(t, f.Reader) + src, err := suite.storage.Put(suite.ctx, album, image, Png()) + assert.NoError(t, err) + assert.Equal(t, "/aye-and-nay/albums/"+ids.Base64(0)+"/images/"+ids.Base64(1), src) + f, err = suite.storage.Get(suite.ctx, album, image) + assert.NoError(t, err) + AssertEqualFile(t, f, Png()) + err = suite.storage.Remove(suite.ctx, album, image) + assert.NoError(t, err) + f, err = suite.storage.Get(suite.ctx, album, image) + e = minioS3.ErrorResponse{} + assert.ErrorAs(t, err, &e) + assert.Nil(t, f.Reader) }) - t.Run("", func(t *testing.T) { - minio, err := NewMinio() - if err != nil { - t.Fatal(err) - } - src, err := minio.Put(context.Background(), 0x872D, 0x882D, Png()) - if err != nil { - t.Error(err) - } - if src != "/aye-and-nay/albums/LYcAAAAAAAA/images/LYgAAAAAAAA" { - t.Error("src != \"/aye-and-nay/albums/LYcAAAAAAAA/images/LYgAAAAAAAA\"") - } - f, err := minio.Get(context.Background(), 0x872D, 0x882D) - if err != nil { - t.Error(err) - } - if !EqualFile(f, Png()) { - t.Error("!EqualFile(f, Png())") - } - err = minio.Remove(context.Background(), 0x872D, 0x882D) - if err != nil { - t.Error(err) - } - src, err = minio.Put(context.Background(), 0x872D, 0x882D, Png()) - if err != nil { - t.Error(err) - } - if src != "/aye-and-nay/albums/LYcAAAAAAAA/images/LYgAAAAAAAA" { - t.Error("src != \"/aye-and-nay/albums/LYcAAAAAAAA/images/LYgAAAAAAAA\"") - } + suite.T().Run("", func(t *testing.T) { + suite.setupTestFn() + id, ids := GenId() + album := id() + image := id() + src, err := suite.storage.Put(suite.ctx, album, image, Png()) + assert.NoError(t, err) + assert.Equal(t, "/aye-and-nay/albums/"+ids.Base64(0)+"/images/"+ids.Base64(1), src) + f, err := suite.storage.Get(suite.ctx, album, image) + assert.NoError(t, err) + AssertEqualFile(t, f, Png()) + err = suite.storage.Remove(suite.ctx, album, image) + assert.NoError(t, err) + src, err = suite.storage.Put(suite.ctx, album, image, Png()) + assert.NoError(t, err) + assert.Equal(t, "/aye-and-nay/albums/"+ids.Base64(0)+"/images/"+ids.Base64(1), src) }) } + +func (suite *MinioTestSuite) TestMinioHealth() { + _, err := suite.storage.Health(suite.ctx) + assert.NoError(suite.T(), err) +} diff --git a/infrastructure/storage/mock.go b/infrastructure/storage/mock.go index a4c069b..bceeba6 100644 --- a/infrastructure/storage/mock.go +++ b/infrastructure/storage/mock.go @@ -1,11 +1,8 @@ package storage import ( - "bytes" "context" "io" - "mime/multipart" - "os" "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/model" @@ -15,6 +12,10 @@ import ( "github.com/zitryss/aye-and-nay/pkg/pool" ) +var ( + _ domain.Storager = (*Mock)(nil) +) + func NewMock() *Mock { return &Mock{} } @@ -23,36 +24,33 @@ type Mock struct { } func (m *Mock) Put(_ context.Context, album uint64, image uint64, f model.File) (string, error) { - defer func() { - switch v := f.Reader.(type) { - case *os.File: - _ = v.Close() - _ = os.Remove(v.Name()) - case multipart.File: - _ = v.Close() - case *bytes.Buffer: - pool.PutBuffer(v) - default: - panic(errors.Wrap(domain.ErrUnknown)) - } - }() + defer f.Close() albumB64 := base64.FromUint64(album) imageB64 := base64.FromUint64(image) filename := "albums/" + albumB64 + "/images/" + imageB64 + _, _ = io.Copy(io.Discard, f.Reader) src := "/aye-and-nay/" + filename return src, nil } func (m *Mock) Get(_ context.Context, _ uint64, _ uint64) (model.File, error) { - buf := pool.GetBuffer() f := Png() - n, err := io.CopyN(buf, f, f.Size) + buf := pool.GetBufferN(f.Size) + n, err := io.Copy(buf, f.Reader) if err != nil { return model.File{}, errors.Wrap(err) } - return model.File{Reader: buf, Size: n}, nil + closeFn := func() error { + pool.PutBuffer(buf) + return nil + } + return model.NewFile(buf, closeFn, n), nil } func (m *Mock) Remove(_ context.Context, _ uint64, _ uint64) error { return nil } + +func (m *Mock) Health(_ context.Context) (bool, error) { + return true, nil +} diff --git a/infrastructure/storage/storage.go b/infrastructure/storage/storage.go index 8f91f3b..84a1ad4 100644 --- a/infrastructure/storage/storage.go +++ b/infrastructure/storage/storage.go @@ -1,15 +1,17 @@ package storage import ( + "context" + "github.com/zitryss/aye-and-nay/domain/domain" - "github.com/zitryss/aye-and-nay/pkg/log" + "github.com/zitryss/aye-and-nay/internal/log" ) -func New(s string) (domain.Storager, error) { - switch s { +func New(ctx context.Context, conf StorageConfig) (domain.Storager, error) { + switch conf.Storage { case "minio": - log.Info("connecting to storage") - return NewMinio() + log.Info(context.Background(), "connecting to storage") + return NewMinio(ctx, conf.Minio) case "mock": return NewMock(), nil default: diff --git a/infrastructure/storage/storage_integration_test.go b/infrastructure/storage/storage_integration_test.go deleted file mode 100644 index 496bea8..0000000 --- a/infrastructure/storage/storage_integration_test.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build integration - -package storage - -import ( - "io" - "os" - "testing" - - "github.com/zitryss/aye-and-nay/internal/dockertest" - "github.com/zitryss/aye-and-nay/pkg/env" - "github.com/zitryss/aye-and-nay/pkg/log" -) - -func TestMain(m *testing.M) { - _, err := env.Lookup("CONTINUOUS_INTEGRATION") - if err != nil { - log.SetOutput(os.Stderr) - log.SetLevel(log.Lcritical) - docker := dockertest.New() - docker.RunMinio() - log.SetOutput(io.Discard) - code := m.Run() - docker.Purge() - os.Exit(code) - } - code := m.Run() - os.Exit(code) -} diff --git a/infrastructure/storage/storage_test.go b/infrastructure/storage/storage_test.go new file mode 100644 index 0000000..119d6e2 --- /dev/null +++ b/infrastructure/storage/storage_test.go @@ -0,0 +1,37 @@ +package storage + +import ( + "flag" + "io" + "os" + "testing" + + "github.com/zitryss/aye-and-nay/internal/dockertest" + "github.com/zitryss/aye-and-nay/pkg/log" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestMain(m *testing.M) { + flag.Parse() + if *ci || !*integration { + code := m.Run() + os.Exit(code) + } + log.SetOutput(os.Stderr) + log.SetLevel(log.CRITICAL) + docker := dockertest.New() + host := &DefaultMinioConfig.Host + port := &DefaultMinioConfig.Port + accessKey := DefaultMinioConfig.AccessKey + secretKey := DefaultMinioConfig.SecretKey + docker.RunMinio(host, port, accessKey, secretKey) + log.SetOutput(io.Discard) + code := m.Run() + docker.Purge() + os.Exit(code) +} diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..d8468e5 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,356 @@ +package client + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "io" + "mime/multipart" + "net" + "net/http" + "os" + "strconv" + "strings" + "sync" + "time" + + "github.com/zitryss/aye-and-nay/pkg/errors" +) + +func New(apiAddress string, timeout time.Duration, opts ...options) (*Client, error) { + c := Client{} + httpTransport := &http.Transport{ + Proxy: http.ProxyFromEnvironment, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + DialContext: (&net.Dialer{ + KeepAlive: 30 * time.Second, + Timeout: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 10 * time.Second, + IdleConnTimeout: 90 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + transport := newTransport(httpTransport, &c.m, &c.passed, &c.failed) + httpClient := &http.Client{ + Jar: nil, + Timeout: timeout, + Transport: transport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + c.client = httpClient + c.apiAddress = apiAddress + for _, opt := range opts { + opt(&c) + } + if c.testdata != "" { + err := c.readFiles() + if err != nil { + return &Client{}, errors.Wrap(err) + } + } + return &c, nil +} + +type options func(*Client) + +func WithFiles(testdata string) options { + return func(c *Client) { + c.testdata = testdata + } +} + +func WithTimes(times int) options { + return func(c *Client) { + c.times = times + } +} + +type Client struct { + client *http.Client + testdata string + apiAddress string + times int + b []byte + sep string + m sync.Mutex + passed int + failed int +} + +func (c *Client) readFiles() error { + body := bytes.Buffer{} + multi := multipart.NewWriter(&body) + for i := 0; i < c.times; i++ { + for _, filename := range []string{"alan.jpg", "john.bmp", "dennis.png"} { + part, err := multi.CreateFormFile("images", filename) + if err != nil { + return errors.Wrap(err) + } + b, err := os.ReadFile(c.testdata + "/" + filename) + if err != nil { + return errors.Wrap(err) + } + _, err = part.Write(b) + if err != nil { + return errors.Wrap(err) + } + } + } + err := multi.WriteField("duration", "1h") + if err != nil { + return errors.Wrap(err) + } + err = multi.Close() + if err != nil { + return errors.Wrap(err) + } + c.b = body.Bytes() + c.sep = multi.FormDataContentType() + return nil +} + +func (c *Client) Album() (string, error) { + body := bytes.NewReader(c.b) + req, err := http.NewRequest(http.MethodPost, c.apiAddress+"/api/albums/", body) + if err != nil { + return "", errors.Wrap(err) + } + req.Header.Set("Content-Type", c.sep) + + resp, err := c.client.Do(req) + if err != nil { + return "", errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return "", errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + + type result struct { + Album struct { + Id string + } + } + + res := result{} + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return "", errors.Wrap(err) + } + + return res.Album.Id, nil +} + +func (c *Client) Status(album string) error { + req, err := http.NewRequest(http.MethodGet, c.apiAddress+"/api/albums/"+album+"/status/", http.NoBody) + if err != nil { + return errors.Wrap(err) + } + + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + + type result struct { + Album struct { + Progress float64 + } + } + + res := result{} + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return errors.Wrap(err) + } + + return nil +} + +type Pair struct { + One elem + Two elem +} + +type elem struct { + Token string + Src string +} + +func (c *Client) Pair(album string) (Pair, error) { + req, err := http.NewRequest(http.MethodGet, c.apiAddress+"/api/albums/"+album+"/pair/", http.NoBody) + if err != nil { + return Pair{}, errors.Wrap(err) + } + + resp, err := c.client.Do(req) + if err != nil { + return Pair{}, errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return Pair{}, errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + + type result struct { + Album struct { + Img1 struct { + Token string + Src string + } + Img2 struct { + Token string + Src string + } + } + } + + res := result{} + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return Pair{}, errors.Wrap(err) + } + + p := Pair{ + One: elem{ + Token: res.Album.Img1.Token, + Src: res.Album.Img1.Src, + }, + Two: elem{ + Token: res.Album.Img2.Token, + Src: res.Album.Img2.Src, + }, + } + return p, nil +} + +func (c *Client) Vote(album string, token1 string, token2 string) error { + body := strings.NewReader("{\"album\":{\"imgFrom\":{\"token\":\"" + token1 + "\"},\"imgTo\":{\"token\":\"" + token2 + "\"}}}") + req, err := http.NewRequest(http.MethodPatch, c.apiAddress+"/api/albums/"+album+"/vote/", body) + if err != nil { + return errors.Wrap(err) + } + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + + return nil +} + +func (c *Client) Top(album string) ([]string, error) { + req, err := http.NewRequest(http.MethodGet, c.apiAddress+"/api/albums/"+album+"/top/", http.NoBody) + if err != nil { + return nil, errors.Wrap(err) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return nil, errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + + type image struct { + Src string + Rating float64 + } + type result struct { + Album struct { + Images []image + } + } + + res := result{} + err = json.NewDecoder(resp.Body).Decode(&res) + if err != nil { + return nil, errors.Wrap(err) + } + + src := []string(nil) + for _, image := range res.Album.Images { + src = append(src, image.Src) + } + return src, nil +} + +func (c *Client) Health() error { + req, err := http.NewRequest(http.MethodGet, c.apiAddress+"/api/health/", http.NoBody) + if err != nil { + return errors.Wrap(err) + } + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + return nil +} + +func (c *Client) Do(method string, url string, body io.ReadCloser) error { + req, err := http.NewRequest(method, url, body) + if err != nil { + return errors.Wrap(err) + } + resp, err := c.client.Do(req) + if err != nil { + return errors.Wrap(err) + } + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + if resp.StatusCode/100 != 2 { + return errors.Wrap(errors.New("response status code: expected = 2xx, actual = " + strconv.Itoa(resp.StatusCode))) + } + return nil +} + +func (c *Client) Stats() (int, int) { + p := 0 + f := 0 + c.m.Lock() + p = c.passed + f = c.failed + c.m.Unlock() + return p, f +} diff --git a/internal/client/transport.go b/internal/client/transport.go new file mode 100644 index 0000000..176b709 --- /dev/null +++ b/internal/client/transport.go @@ -0,0 +1,42 @@ +package client + +import ( + "net/http" + "sync" +) + +func newTransport(roundTrip http.RoundTripper, m *sync.Mutex, passed *int, failed *int) *transport { + t := transport{} + t.original = roundTrip + t.m = m + t.passed = passed + t.failed = failed + return &t +} + +type transport struct { + original http.RoundTripper + m *sync.Mutex + passed *int + failed *int +} + +func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := t.original.RoundTrip(req) + if err != nil { + t.m.Lock() + *t.failed++ + t.m.Unlock() + return resp, err + } + if resp.StatusCode/100 != 2 { + t.m.Lock() + *t.failed++ + t.m.Unlock() + return resp, err + } + t.m.Lock() + *t.passed++ + t.m.Unlock() + return resp, err +} diff --git a/internal/config/config.go b/internal/config/config.go index dabb422..7f5bd27 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,48 +1,135 @@ package config import ( + "context" + "reflect" + "strings" + "time" + + "github.com/go-playground/validator" + "github.com/radovskyb/watcher" "github.com/spf13/viper" - "github.com/zitryss/aye-and-nay/pkg/unit" + "github.com/zitryss/aye-and-nay/delivery/http" + "github.com/zitryss/aye-and-nay/domain/service" + "github.com/zitryss/aye-and-nay/infrastructure/cache" + "github.com/zitryss/aye-and-nay/infrastructure/compressor" + "github.com/zitryss/aye-and-nay/infrastructure/database" + "github.com/zitryss/aye-and-nay/infrastructure/storage" + "github.com/zitryss/aye-and-nay/internal/log" + "github.com/zitryss/aye-and-nay/pkg/errors" ) -func init() { - viper.Set("middleware.limiter.requestsPerSecond", 100) - viper.Set("middleware.limiter.burst", 1) - viper.Set("controller.maxNumberOfFiles", 3) - viper.Set("controller.maxFileSize", 512*unit.KB) - viper.Set("service.numberOfWorkersCalc", 2) - viper.Set("service.numberOfWorkersComp", 2) - viper.Set("service.accuracy", 0.625) - viper.Set("cache.redis.host", "localhost") - viper.Set("cache.redis.port", 6379) - viper.Set("cache.redis.retry.times", 4) - viper.Set("cache.redis.retry.pause", "5s") - viper.Set("cache.redis.retry.timeout", "30s") - viper.Set("cache.redis.timeToLive", "1s") - viper.Set("compressor.use", "notamock") - viper.Set("compressor.imaginary.host", "localhost") - viper.Set("compressor.imaginary.port", 9001) - viper.Set("compressor.imaginary.retry.times", 4) - viper.Set("compressor.imaginary.retry.pause", "5s") - viper.Set("compressor.imaginary.retry.timeout", "30s") - viper.Set("compressor.shortpixel.uploadTimeout", "250ms") - viper.Set("compressor.shortpixel.downloadTimeout", "250ms") - viper.Set("database.mongo.host", "localhost") - viper.Set("database.mongo.port", 27017) - viper.Set("database.mongo.retry.times", 4) - viper.Set("database.mongo.retry.pause", "5s") - viper.Set("database.mongo.retry.timeout", "30s") - viper.Set("database.mongo.lru", 1) - viper.Set("database.badger.lru", 1) - viper.Set("storage.minio.host", "localhost") - viper.Set("storage.minio.port", 9000) - viper.Set("storage.minio.accessKey", "12345678") - viper.Set("storage.minio.secretKey", "qwertyui") - viper.Set("storage.minio.token", "") - viper.Set("storage.minio.secure", false) - viper.Set("storage.minio.retry.times", 4) - viper.Set("storage.minio.retry.pause", "5s") - viper.Set("storage.minio.retry.timeout", "30s") - viper.Set("storage.minio.location", "eu-central-1") +func New(path string) (Config, error) { + viper.Reset() + conf := Config{} + conf.path = path + err := readConfig(path, &conf) + if err != nil { + return Config{}, errors.Wrap(err) + } + fillGaps(&conf) + err = validator.New().Struct(conf) + if err != nil { + return Config{}, errors.Wrap(err) + } + return conf, nil +} + +type Config struct { + path string + Reload bool `mapstructure:"CONFIG_RELOAD"` + ReloadInterval time.Duration `mapstructure:"CONFIG_RELOAD_INTERVAL" validate:"required"` + App AppConfig `mapstructure:",squash"` + Server http.ServerConfig `mapstructure:",squash"` + Middleware http.MiddlewareConfig `mapstructure:",squash"` + Service service.ServiceConfig `mapstructure:",squash"` + Cache cache.CacheConfig `mapstructure:",squash"` + Compressor compressor.CompressorConfig `mapstructure:",squash"` + Database database.DatabaseConfig `mapstructure:",squash"` + Storage storage.StorageConfig `mapstructure:",squash"` +} + +type AppConfig struct { + Name string `mapstructure:"APP_NAME" validate:"required"` + Log string `mapstructure:"APP_LOG" validate:"required"` + GcTuner string `mapstructure:"APP_GC_TUNER" validate:"required"` + MemTotal int `mapstructure:"APP_MEM_TOTAL"` + MemLimitRatio float64 `mapstructure:"APP_MEM_LIMIT_RATIO"` +} + +func (c *Config) OnChange(ctx context.Context, fn func()) { + w := watcher.New() + w.SetMaxEvents(1) + w.FilterOps(watcher.Write) + err := w.Add(c.path) + if err != nil { + log.Error(context.Background(), "err", "stacktrace", errors.Wrap(err)) + } + go func() { + for { + select { + case <-w.Event: + fn() + case err := <-w.Error: + log.Error(context.Background(), "err", "stacktrace", errors.Wrap(err)) + case <-w.Closed: + return + case <-ctx.Done(): + w.Wait() + w.Close() + return + } + } + }() + go func() { + err := w.Start(c.ReloadInterval) + if err != nil { + log.Error(context.Background(), "err", "stacktrace", errors.Wrap(err)) + } + }() +} + +func readConfig(path string, conf *Config) error { + viper.SetConfigFile(path) + viper.AutomaticEnv() + err := viper.ReadInConfig() + if err != nil { + log.Error(context.Background(), "err", "stacktrace", errors.Wrap(err)) + } + if len(viper.AllSettings()) == 0 { + bindEnv(reflect.TypeOf(*conf)) + } + if len(viper.AllSettings()) == 0 { + return errors.Wrap(errors.New("no configuration is provided")) + } + err = viper.Unmarshal(conf) + if err != nil { + return errors.Wrap(err) + } + return nil +} + +func bindEnv(t reflect.Type) { + if t.Kind() != reflect.Struct { + return + } + for _, field := range reflect.VisibleFields(t) { + bindEnv(field.Type) + tag := field.Tag.Get("mapstructure") + if field.IsExported() && tag != "" && tag != ",squash" { + err := viper.BindEnv(strings.ToLower(tag), tag) + if err != nil { + log.Error(context.Background(), "err", "stacktrace", errors.Wrap(err)) + } + } + } +} + +func fillGaps(conf *Config) { + if conf.Compressor.IsMock() { + conf.Database.Mem.Compressed = true + conf.Database.Mongo.Compressed = true + conf.Database.Badger.Compressed = true + } } diff --git a/internal/dockertest/dockertest.go b/internal/dockertest/dockertest.go index eb52ac2..9b323bb 100644 --- a/internal/dockertest/dockertest.go +++ b/internal/dockertest/dockertest.go @@ -5,22 +5,20 @@ import ( "os" "github.com/ory/dockertest/v3" - "github.com/spf13/viper" - "github.com/zitryss/aye-and-nay/pkg/env" "github.com/zitryss/aye-and-nay/pkg/errors" "github.com/zitryss/aye-and-nay/pkg/log" ) func New() docker { - host, err := env.Lookup("DOCKER_HOST") - if err != nil { + host, ok := os.LookupEnv("DOCKER_HOST") + if !ok || host == "" { host = "tcp://localhost:2375" } u, err := url.Parse(host) if err != nil { err = errors.Wrap(err) - log.Critical("dockertest: ", err) + log.Critical("dockertest:", err) os.Exit(1) } hostname := u.Hostname() @@ -40,58 +38,56 @@ type docker struct { resources []*dockertest.Resource } -func (d *docker) RunRedis() { +func (d *docker) RunRedis(host *string, hPort *string) { repository := "redis" tag := "6-alpine" env := []string(nil) cmd := []string(nil) - port := "6379/tcp" + cPort := "6379/tcp" conf := func(port string) { - viper.Set("cache.redis.host", d.host) - viper.Set("cache.redis.port", port) + *host = d.host + *hPort = port } - d.run(repository, tag, env, cmd, port, conf) + d.run(repository, tag, env, cmd, cPort, conf) } -func (d *docker) RunImaginary() { +func (d *docker) RunImaginary(host *string, hPort *string) { repository := "h2non/imaginary" tag := "1" env := []string(nil) cmd := []string(nil) - port := "9000/tcp" + cPort := "9000/tcp" conf := func(port string) { - viper.Set("compressor.imaginary.host", d.host) - viper.Set("compressor.imaginary.port", port) + *host = d.host + *hPort = port } - d.run(repository, tag, env, cmd, port, conf) + d.run(repository, tag, env, cmd, cPort, conf) } -func (d *docker) RunMongo() { +func (d *docker) RunMongo(host *string, hPort *string) { repository := "mongo" tag := "5" env := []string(nil) cmd := []string(nil) - port := "27017/tcp" + cPort := "27017/tcp" conf := func(port string) { - viper.Set("database.mongo.host", d.host) - viper.Set("database.mongo.port", port) + *host = d.host + *hPort = port } - d.run(repository, tag, env, cmd, port, conf) + d.run(repository, tag, env, cmd, cPort, conf) } -func (d *docker) RunMinio() { +func (d *docker) RunMinio(host *string, hPort *string, accessKey string, secretKey string) { repository := "minio/minio" tag := "RELEASE.2021-11-24T23-19-33Z" - accessKey := viper.GetString("storage.minio.accessKey") - secretKey := viper.GetString("storage.minio.secretKey") env := []string{"MINIO_ACCESS_KEY=" + accessKey, "MINIO_SECRET_KEY=" + secretKey} cmd := []string{"server", "/data"} - port := "9000/tcp" + cPort := "9000/tcp" conf := func(port string) { - viper.Set("storage.minio.host", d.host) - viper.Set("storage.minio.port", port) + *host = d.host + *hPort = port } - d.run(repository, tag, env, cmd, port, conf) + d.run(repository, tag, env, cmd, cPort, conf) } func (d *docker) run(repository string, tag string, env []string, cmd []string, containerPort string, conf func(string)) { diff --git a/internal/gctuner/gctuner.go b/internal/gctuner/gctuner.go new file mode 100644 index 0000000..e59ec0d --- /dev/null +++ b/internal/gctuner/gctuner.go @@ -0,0 +1,197 @@ +package gctuner + +import ( + "context" + "io" + "os" + "runtime" + "runtime/debug" + "strconv" + "strings" + + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/process" + "github.com/spf13/afero" + + "github.com/zitryss/aye-and-nay/internal/log" + "github.com/zitryss/aye-and-nay/pkg/errors" +) + +const ( + cgroupMemTotalPathV1 = "/sys/fs/cgroup/memory/memory.limit_in_bytes" + cgroupMemTotalPathV2 = "/sys/fs/cgroup/memory.max" +) + +var ( + newGOGC float64 + lastGOGC float64 + memTotal float64 + memLimitRatio = 0.7 + appFs = afero.NewOsFs() + memUsedFn = memProcess +) + +func Start(ctx context.Context, total int, ratio float64) error { + if lastGOGC == 0.0 { + gogc, ok := os.LookupEnv("GOGC") + if !ok || gogc == "" { + gogc = "100" + } + err := error(nil) + lastGOGC, err = strconv.ParseFloat(gogc, 64) + if err != nil { + return errors.Wrap(err) + } + } + if total > 0.0 { + memTotal = float64(total) + } + if ratio > 0.0 && ratio <= 1.0 { + memLimitRatio = ratio + } + err := checkMemTotal() + if err != nil { + return errors.Wrap(err) + } + err = checkCgroup() + if err != nil { + return errors.Wrap(err) + } + fin := &finalizer{} + fin.ref = &finalizerRef{parent: fin} + runtime.SetFinalizer(fin.ref, finalizerHandler(ctx)) + fin.ref = nil + return nil +} + +func checkMemTotal() error { + if memTotal > 0.0 { + return nil + } + memVirtual, err := mem.VirtualMemory() + if err != nil { + return errors.Wrap(err) + } + memTotal = float64(memVirtual.Total) + return nil +} + +func checkCgroup() error { + var ( + f io.ReadCloser + err error + e error + mt float64 + ) + f, err = appFs.Open(cgroupMemTotalPathV1) + if err != nil { + e = errors.Wrap(err) + goto second_file + } + defer f.Close() + mt, err = readCgroupMemTotal(f) + if err != nil { + e = errors.Wrap(err) + goto second_file + } + if mt > 0.0 && mt < memTotal { + memTotal = mt + } +second_file: + f, err = appFs.Open(cgroupMemTotalPathV2) + if err != nil { + return nil + } + defer f.Close() + mt, err = readCgroupMemTotal(f) + if err != nil && e != nil { + return errors.Wrapf(err, "%s", e) + } + if err != nil && e == nil { + return nil + } + if mt > 0.0 && mt < memTotal { + memTotal = mt + } + return nil +} + +func readCgroupMemTotal(f io.Reader) (float64, error) { + b, err := io.ReadAll(f) + if err != nil { + return 0.0, errors.Wrap(err) + } + s := strings.TrimSpace(string(b)) + if s == "" || s == "max" { + return 0.0, nil + } + cgroupMemTotal, err := strconv.ParseFloat(s, 64) + if err != nil { + return 0.0, errors.Wrap(err) + } + if cgroupMemTotal <= 0.0 { + return 0.0, nil + } + return cgroupMemTotal, nil +} + +type finalizer struct { + ref *finalizerRef +} + +type finalizerRef struct { + parent *finalizer +} + +func finalizerHandler(ctx context.Context) func(fin *finalizerRef) { + return func(fin *finalizerRef) { + err := updateGOGC() + if err != nil { + log.Error(context.Background(), "err", "stacktrace", err) + } + select { + case <-ctx.Done(): + return + default: + runtime.SetFinalizer(fin, finalizerHandler(ctx)) + } + } +} + +func updateGOGC() error { + memUsed, err := memUsedFn() + if err != nil { + return errors.Wrap(err) + } + if memTotal == 0 { + return errors.New("division by zero") + } + memUsedRatio := memUsed / memTotal + if memUsedRatio == 0 { + return errors.New("division by zero") + } + newGOGC = (memLimitRatio - memUsedRatio) / memUsedRatio * 100.0 + if newGOGC < 0.0 { + newGOGC = lastGOGC * memLimitRatio / memUsedRatio + } + lastGOGC = float64(debug.SetGCPercent(int(newGOGC))) + log.Debug(context.Background(), + "gc", + "mem used", memUsed, + "mem used ratio", memUsedRatio, + "new GOGC", newGOGC, + ) + return nil +} + +func memProcess() (float64, error) { + p, err := process.NewProcess(int32(os.Getpid())) + if err != nil { + return 0.0, errors.Wrap(err) + } + processMemory, err := p.MemoryInfo() + if err != nil { + return 0.0, errors.Wrap(err) + } + return float64(processMemory.RSS), nil +} diff --git a/internal/gctuner/gctuner_test.go b/internal/gctuner/gctuner_test.go new file mode 100644 index 0000000..d43496f --- /dev/null +++ b/internal/gctuner/gctuner_test.go @@ -0,0 +1,293 @@ +package gctuner + +import ( + "flag" + "io" + "runtime/debug" + "strings" + "testing" + + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + + . "github.com/zitryss/aye-and-nay/internal/testing" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestReadCgroupMemTotal(t *testing.T) { + if !*unit { + t.Skip() + } + tests := []struct { + f io.Reader + want float64 + wantErr bool + }{ + { + f: strings.NewReader(""), + want: 0, + wantErr: false, + }, + { + f: strings.NewReader("0"), + want: 0, + wantErr: false, + }, + { + f: strings.NewReader("1"), + want: 1, + wantErr: false, + }, + { + f: strings.NewReader("\r1\n"), + want: 1, + wantErr: false, + }, + { + f: strings.NewReader("-1"), + want: 0, + wantErr: false, + }, + { + f: strings.NewReader("9223372036854775807"), + want: 9223372036854775807, + wantErr: false, + }, + { + f: strings.NewReader("-9223372036854775808"), + want: 0, + wantErr: false, + }, + { + f: strings.NewReader("max"), + want: 0, + wantErr: false, + }, + { + f: strings.NewReader("abc"), + want: 0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got, err := readCgroupMemTotal(tt.f) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestMemTotal(t *testing.T) { + if !*unit { + t.Skip() + } + tests := []struct { + total float64 + cgroupMemTotalV1 []byte + cgroupMemTotalV2 []byte + want float64 + wantErr bool + }{ + { + total: 1073741824, + cgroupMemTotalV1: []byte(""), + cgroupMemTotalV2: []byte("-1"), + want: 1073741824, + wantErr: false, + }, + { + total: 1073741824, + cgroupMemTotalV1: []byte("$%^&"), + cgroupMemTotalV2: []byte("0"), + want: 1073741824, + wantErr: false, + }, + { + total: 1073741824, + cgroupMemTotalV1: []byte("max"), + cgroupMemTotalV2: []byte("$%^&"), + want: 1073741824, + wantErr: false, + }, + { + total: 0, + cgroupMemTotalV1: []byte("943718400\n"), + cgroupMemTotalV2: nil, + want: 943718400, + wantErr: false, + }, + { + total: 0, + cgroupMemTotalV1: nil, + cgroupMemTotalV2: []byte("734003200\n"), + want: 734003200, + wantErr: false, + }, + { + total: 1073741824, + cgroupMemTotalV1: []byte("943718400\n"), + cgroupMemTotalV2: nil, + want: 943718400, + wantErr: false, + }, + { + total: 943718400, + cgroupMemTotalV1: []byte("1073741824\n"), + cgroupMemTotalV2: nil, + want: 943718400, + wantErr: false, + }, + { + total: 1073741824, + cgroupMemTotalV1: nil, + cgroupMemTotalV2: []byte("943718400\n"), + want: 943718400, + wantErr: false, + }, + { + total: 943718400, + cgroupMemTotalV1: nil, + cgroupMemTotalV2: []byte("1073741824\n"), + want: 943718400, + wantErr: false, + }, + { + total: 0, + cgroupMemTotalV1: []byte("943718400\n"), + cgroupMemTotalV2: []byte("1073741824\n"), + want: 943718400, + wantErr: false, + }, + { + total: 0, + cgroupMemTotalV1: []byte("1073741824\n"), + cgroupMemTotalV2: []byte("943718400\n"), + want: 943718400, + wantErr: false, + }, + { + total: 734003200, + cgroupMemTotalV1: []byte("1073741824\n"), + cgroupMemTotalV2: []byte("943718400\n"), + want: 734003200, + wantErr: false, + }, + { + total: 943718400, + cgroupMemTotalV1: []byte("1073741824\n"), + cgroupMemTotalV2: []byte("734003200\n"), + want: 734003200, + wantErr: false, + }, + { + total: 1073741824, + cgroupMemTotalV1: []byte("734003200\n"), + cgroupMemTotalV2: []byte("943718400\n"), + want: 734003200, + wantErr: false, + }, + { + total: 1073741824, + cgroupMemTotalV1: []byte("$%^&"), + cgroupMemTotalV2: []byte("$%^&"), + want: 0.0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + appFs = afero.NewMemMapFs() + if tt.cgroupMemTotalV1 != nil { + err := afero.WriteFile(appFs, cgroupMemTotalPathV1, tt.cgroupMemTotalV1, 0644) + assert.NoError(t, err) + } + if tt.cgroupMemTotalV2 != nil { + err := afero.WriteFile(appFs, cgroupMemTotalPathV2, tt.cgroupMemTotalV2, 0644) + assert.NoError(t, err) + } + memTotal = tt.total + err := checkMemTotal() + assert.NoError(t, err) + err = checkCgroup() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, memTotal) + }) + } +} + +func TestUpdateGOGC(t *testing.T) { + if !*unit { + t.Skip() + } + memTestFn := func(memUsed float64) func() (float64, error) { + return func() (float64, error) { + return memUsed, nil + } + } + tests := []struct { + memTotal float64 + memUsed float64 + memLimitRatio float64 + want float64 + wantErr bool + }{ + { + memTotal: 100.0, + memUsed: 20.0, + memLimitRatio: 0.6, + want: 199.99999999999997, + wantErr: false, + }, + { + memTotal: 100.0, + memUsed: 99.9, + memLimitRatio: 0.5, + want: 99.59959959959959, + wantErr: false, + }, + { + memTotal: 0.0, + memUsed: 20.0, + memLimitRatio: 0.7, + want: 0.0, + wantErr: true, + }, + { + memTotal: 100.0, + memUsed: 0.0, + memLimitRatio: 0.7, + want: 0.0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + memTotal = tt.memTotal + memUsedFn = memTestFn(tt.memUsed) + memLimitRatio = tt.memLimitRatio + lastGOGC = float64(debug.SetGCPercent(100)) + err := updateGOGC() + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + got := newGOGC + assert.InDelta(t, tt.want, got, TOLERANCE) + }) + } +} diff --git a/internal/generator/generator.go b/internal/generator/generator.go new file mode 100644 index 0000000..874b3e3 --- /dev/null +++ b/internal/generator/generator.go @@ -0,0 +1,87 @@ +package generator + +import ( + "fmt" + "sync" + + "github.com/zitryss/aye-and-nay/pkg/base64" +) + +const ( + span = 100 +) + +var ( + m sync.Mutex + indexId uint64 +) + +type IdGenFunc func() uint64 + +type Ids interface { + Uint64(i int) uint64 + Base64(i int) string +} + +func GenId() (IdGenFunc, *IdLogBook) { + lb := IdLogBook{m: sync.Mutex{}, logBook: map[int]uint64{}, valid: true} + fn := func() IdGenFunc { + m.Lock() + firstId := indexId + indexId += span + m.Unlock() + mFn := sync.Mutex{} + i := -1 + curId := firstId - 1 + lastId := firstId + span - 1 + return func() uint64 { + mFn.Lock() + i++ + curId++ + if curId > lastId { + panic("id out of bounds") + } + lb.set(i, curId) + id := curId + mFn.Unlock() + return id + } + } + return fn(), &lb +} + +type IdLogBook struct { + m sync.Mutex + logBook map[int]uint64 + valid bool +} + +func (lb *IdLogBook) set(i int, id uint64) { + if lb == nil || !lb.valid { + return + } + lb.m.Lock() + defer lb.m.Unlock() + _, ok := lb.logBook[i] + if ok { + panic(fmt.Sprintf("id #%d already used", i)) + } + lb.logBook[i] = id +} + +func (lb *IdLogBook) Uint64(i int) uint64 { + if lb == nil || !lb.valid { + return 0x0 + } + lb.m.Lock() + defer lb.m.Unlock() + id, ok := lb.logBook[i] + if !ok { + panic(fmt.Sprintf("id #%d not found", i)) + } + return id +} + +func (lb *IdLogBook) Base64(i int) string { + return base64.FromUint64(lb.Uint64(i)) +} diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go new file mode 100644 index 0000000..6b4bf37 --- /dev/null +++ b/internal/generator/generator_test.go @@ -0,0 +1,59 @@ +package generator + +import ( + "flag" + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestGenId(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() + id, ids := GenId() + id, ids = GenId() + wg := sync.WaitGroup{} + wg.Add(2) + ch1 := make(chan struct{}, 1) + ch2 := make(chan struct{}, 1) + assert.NotPanics(t, func() { + go func() { + defer wg.Done() + for i := 0; i < span/2; i++ { + _ = id() + } + ch1 <- struct{}{} + <-ch2 + for i := 0; i < span/2; i++ { + _ = ids.Uint64(i) + } + }() + go func() { + defer wg.Done() + for i := span / 2; i < span; i++ { + _ = id() + } + ch2 <- struct{}{} + <-ch1 + for i := span / 2; i < span; i++ { + _ = ids.Uint64(i) + } + }() + }) + wg.Wait() + assert.Len(t, ids.logBook, span) + assert.Panics(t, func() { id() }) + assert.Panics(t, func() { + _, ids := GenId() + _ = ids.Base64(0) + }) +} diff --git a/internal/log/log.go b/internal/log/log.go new file mode 100644 index 0000000..85a73e2 --- /dev/null +++ b/internal/log/log.go @@ -0,0 +1,112 @@ +package log + +import ( + "context" + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/zitryss/aye-and-nay/domain/domain" + "github.com/zitryss/aye-and-nay/internal/requestid" + "github.com/zitryss/aye-and-nay/pkg/errors" +) + +var ( + l logger +) + +type logger struct { + *zap.SugaredLogger + lvl zapcore.Level +} + +func New(lvl string, prefix string) error { + zapConf := zap.NewProductionConfig() + zapConf.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder + switch strings.ToLower(lvl) { + case "debug": + l.lvl = zap.DebugLevel + zapConf.Level = zap.NewAtomicLevelAt(l.lvl) + case "info": + l.lvl = zap.InfoLevel + zapConf.Level = zap.NewAtomicLevelAt(l.lvl) + case "error": + l.lvl = zap.WarnLevel + zapConf.Level = zap.NewAtomicLevelAt(l.lvl) + case "critical": + l.lvl = zap.ErrorLevel + zapConf.Level = zap.NewAtomicLevelAt(l.lvl) + default: + return errors.Wrap(errors.New("wrong log level")) + } + zapLog, err := zapConf.Build(zap.WithCaller(false)) + if err != nil { + return errors.Wrap(err) + } + l.SugaredLogger = zapLog.Sugar() + l.With("app-name", prefix) + return nil +} + +func newWithLogger(zapLog *zap.Logger, lvl zapcore.Level) { + l.SugaredLogger = zapLog.Sugar() + l.lvl = lvl +} + +func Print(ctx context.Context, level int, msg string, v ...any) { + switch level { + case domain.LogDebug: + Debug(ctx, msg, v...) + case domain.LogInfo: + Info(ctx, msg, v...) + case domain.LogError: + Error(ctx, msg, v...) + case domain.LogCritical: + Critical(ctx, msg, v...) + } +} + +func Debug(ctx context.Context, msg string, v ...any) { + if l.lvl > zapcore.DebugLevel || l.SugaredLogger == nil { + return + } + requestId := requestid.Get(ctx) + if requestId != 0 { + v = append([]any{"request-id", requestId}, v...) + } + l.Debugw(msg, v...) +} + +func Info(ctx context.Context, msg string, v ...any) { + if l.lvl > zapcore.InfoLevel || l.SugaredLogger == nil { + return + } + requestId := requestid.Get(ctx) + if requestId != 0 { + v = append([]any{"request-id", requestId}, v...) + } + l.Infow(msg, v...) +} + +func Error(ctx context.Context, msg string, v ...any) { + if l.lvl > zapcore.WarnLevel || l.SugaredLogger == nil { + return + } + requestId := requestid.Get(ctx) + if requestId != 0 { + v = append([]any{"request-id", requestId}, v...) + } + l.Warnw(msg, v...) +} + +func Critical(ctx context.Context, msg string, v ...any) { + if l.lvl > zapcore.ErrorLevel || l.SugaredLogger == nil { + return + } + requestId := requestid.Get(ctx) + if requestId != 0 { + v = append([]any{"request-id", requestId}, v...) + } + l.Errorw(msg, v...) +} diff --git a/internal/log/log_test.go b/internal/log/log_test.go new file mode 100644 index 0000000..9d02c95 --- /dev/null +++ b/internal/log/log_test.go @@ -0,0 +1,66 @@ +package log + +import ( + "context" + "flag" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + unit = flag.Bool("unit", true, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func testTimeEncoder(_ time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString("") +} + +func TestLogLevelPositive(t *testing.T) { + if !*unit { + t.Skip() + } + tests := []struct { + level zapcore.Level + want string + }{ + { + level: zapcore.DebugLevel, + want: "\tDEBUG\tmessage1\n\tINFO\tmessage3\n\tWARN\tmessage5\n\tERROR\tmessage7\n", + }, + { + level: zapcore.InfoLevel, + want: "\tINFO\tmessage3\n\tWARN\tmessage5\n\tERROR\tmessage7\n", + }, + { + level: zapcore.WarnLevel, + want: "\tWARN\tmessage5\n\tERROR\tmessage7\n", + }, + { + level: zapcore.ErrorLevel, + want: "\tERROR\tmessage7\n", + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + encConf := zap.NewDevelopmentEncoderConfig() + encConf.EncodeTime = testTimeEncoder + enc := zapcore.NewConsoleEncoder(encConf) + w := strings.Builder{} + l := zap.New(zapcore.NewCore(enc, zapcore.AddSync(&w), tt.level)) + newWithLogger(l, tt.level) + Debug(context.Background(), "message1") + Info(context.Background(), "message3") + Error(context.Background(), "message5") + Critical(context.Background(), "message7") + got := w.String() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/requestid/requestid.go b/internal/requestid/requestid.go new file mode 100644 index 0000000..edf2402 --- /dev/null +++ b/internal/requestid/requestid.go @@ -0,0 +1,35 @@ +package requestid + +import ( + "context" + + "go.uber.org/atomic" +) + +type ctxKey int + +const ( + ctxRequestId ctxKey = iota +) + +var ( + requestID atomic.Uint64 +) + +func Set(ctx context.Context) context.Context { + if ctx == nil { + ctx = context.Background() + } + return context.WithValue(ctx, ctxRequestId, requestID.Inc()) +} + +func Get(ctx context.Context) uint64 { + if ctx == nil { + return 0 + } + reqId, ok := ctx.Value(ctxRequestId).(uint64) + if !ok { + return 0 + } + return reqId +} diff --git a/internal/requestid/requestid_test.go b/internal/requestid/requestid_test.go new file mode 100644 index 0000000..708b359 --- /dev/null +++ b/internal/requestid/requestid_test.go @@ -0,0 +1,37 @@ +package requestid + +import ( + "context" + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestContext(t *testing.T) { + if !*unit { + t.Skip() + } + t.Run("Positive", func(t *testing.T) { + ctx := Set(context.Background()) + id1 := Get(ctx) + assert.NotZero(t, id1) + assert.Positive(t, id1) + assert.Greater(t, id1, uint64(0)) + ctx = Set(ctx) + id2 := Get(ctx) + assert.NotZero(t, id2) + assert.Positive(t, id2) + assert.Greater(t, id2, id1) + }) + t.Run("Negative", func(t *testing.T) { + ctx := context.Background() + assert.Equal(t, uint64(0), Get(ctx)) + }) +} diff --git a/internal/testing/testing.go b/internal/testing/testing.go index 16b9b53..26121e1 100644 --- a/internal/testing/testing.go +++ b/internal/testing/testing.go @@ -3,18 +3,18 @@ package testing import ( "bytes" _ "embed" - "math" "net/http/httptest" - "reflect" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/zitryss/aye-and-nay/domain/model" - "github.com/zitryss/aye-and-nay/pkg/base64" + . "github.com/zitryss/aye-and-nay/internal/generator" ) const ( - tolerance = 0.000000000000001 + TOLERANCE = 0.000000000000001 ) var ( @@ -22,7 +22,31 @@ var ( png []byte ) -func CheckStatusCode(t *testing.T, w *httptest.ResponseRecorder, code int) { +func Png() model.File { + buf := bytes.NewBuffer(png) + return model.File{Reader: buf, Size: int64(buf.Len())} +} + +func AlbumFactory(id IdGenFunc, ids Ids) model.Album { + album := id() + img1 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(1), Rating: 0.48954984} + img2 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(2), Rating: 0.19186324} + img3 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(3), Rating: 0.41218211} + img4 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(4), Rating: 0.77920413} + img5 := model.Image{Id: id(), Src: "/aye-and-nay/albums/" + ids.Base64(0) + "/images/" + ids.Base64(5), Rating: 0.13278389} + imgs := []model.Image{img1, img2, img3, img4, img5} + edgs := map[uint64]map[uint64]int{} + edgs[ids.Uint64(1)] = map[uint64]int{} + edgs[ids.Uint64(2)] = map[uint64]int{} + edgs[ids.Uint64(3)] = map[uint64]int{} + edgs[ids.Uint64(4)] = map[uint64]int{} + edgs[ids.Uint64(5)] = map[uint64]int{} + expires := time.Time{} + alb := model.Album{album, imgs, edgs, expires} + return alb +} + +func AssertStatusCode(t *testing.T, w *httptest.ResponseRecorder, code int) { t.Helper() got := w.Code want := code @@ -31,16 +55,16 @@ func CheckStatusCode(t *testing.T, w *httptest.ResponseRecorder, code int) { } } -func CheckContentType(t *testing.T, w *httptest.ResponseRecorder, content string) { +func AssertHeader(t *testing.T, w *httptest.ResponseRecorder, header string, value string) { t.Helper() - got := w.Result().Header.Get("Content-Type") - want := content + got := w.Result().Header.Get(header) + want := value if got != want { - t.Errorf("Content-Type = %v; want %v", got, want) + t.Errorf("%v = %v; want %v", header, got, want) } } -func CheckBody(t *testing.T, w *httptest.ResponseRecorder, body string) { +func AssertBody(t *testing.T, w *httptest.ResponseRecorder, body string) { t.Helper() got := w.Body.String() want := body @@ -49,107 +73,39 @@ func CheckBody(t *testing.T, w *httptest.ResponseRecorder, body string) { } } -func CheckChannel(t *testing.T, heartbeat <-chan interface{}) interface{} { +func AssertChannel(t *testing.T, heartbeat <-chan any) any { t.Helper() - v := interface{}(nil) + if heartbeat == nil { + return nil + } + v := any(nil) select { case v = <-heartbeat: case <-time.After(1 * time.Second): - t.Fatal("<-time.After(1 * time.Second)") + t.Error("<-time.After(1 * time.Second)") } return v } -func IsIn(image model.Image, imgs []model.Image) bool { - for _, img := range imgs { - if reflect.DeepEqual(image, img) { - return true - } - } - return false -} - -func AlbumEmptyFactory(id uint64) model.Album { - idB64 := base64.FromUint64(id) - img1 := model.Image{Id: 0x3E3D, Src: "/aye-and-nay/albums/" + idB64 + "/images/PT4AAAAAAAA"} - img2 := model.Image{Id: 0xB399, Src: "/aye-and-nay/albums/" + idB64 + "/images/mbMAAAAAAAA"} - img3 := model.Image{Id: 0xDA2A, Src: "/aye-and-nay/albums/" + idB64 + "/images/KtoAAAAAAAA"} - img4 := model.Image{Id: 0x51DE, Src: "/aye-and-nay/albums/" + idB64 + "/images/3lEAAAAAAAA"} - img5 := model.Image{Id: 0xDA52, Src: "/aye-and-nay/albums/" + idB64 + "/images/UtoAAAAAAAA"} - imgs := []model.Image{img1, img2, img3, img4, img5} - edgs := map[uint64]map[uint64]int{} - edgs[0x3E3D] = map[uint64]int{} - edgs[0xB399] = map[uint64]int{} - edgs[0xDA2A] = map[uint64]int{} - edgs[0x51DE] = map[uint64]int{} - edgs[0xDA52] = map[uint64]int{} - expires := time.Time{} - alb := model.Album{id, imgs, edgs, expires} - return alb -} - -func AlbumFullFactory(id uint64) model.Album { - alb := AlbumEmptyFactory(id) - alb.Images[0].Rating = 0.48954984 - alb.Images[1].Rating = 0.19186324 - alb.Images[2].Rating = 0.41218211 - alb.Images[3].Rating = 0.77920413 - alb.Images[4].Rating = 0.13278389 - alb.Edges[0x51DE][0xDA2A]++ - alb.Edges[0x3E3D][0xDA2A]++ - alb.Edges[0x3E3D][0x51DE]++ - alb.Edges[0xB399][0xDA2A]++ - alb.Edges[0xB399][0x51DE]++ - alb.Edges[0xB399][0x3E3D]++ - alb.Edges[0xDA52][0xDA2A]++ - alb.Edges[0xDA52][0x51DE]++ - alb.Edges[0xDA52][0x3E3D]++ - alb.Edges[0xDA52][0xB399]++ - return alb -} - -func EqualMap(x, y map[uint64]float64) bool { - if len(x) != len(y) { - return false - } - for xk, xv := range x { - yv, ok := y[xk] - if !ok { - return false - } - if !EqualFloat(xv, yv) { - return false - } +func AssertNotChannel(t *testing.T, heartbeat <-chan any) { + t.Helper() + if heartbeat == nil { + return } - return true -} - -func EqualFloat(x, y float64) bool { - diff := math.Abs(x - y) - if diff > tolerance { - return false + select { + case <-heartbeat: + t.Error("<-heartbeatDel") + case <-time.After(1 * time.Second): } - return true -} - -func Png() model.File { - buf := bytes.NewBuffer(png) - return model.File{Reader: buf, Size: int64(buf.Len())} } -func EqualFile(x, y model.File) bool { +func AssertEqualFile(t *testing.T, x, y model.File) { + t.Helper() bx := make([]byte, x.Size) _, err := x.Read(bx) - if err != nil { - return false - } + assert.NoError(t, err) by := make([]byte, y.Size) _, err = y.Read(by) - if err != nil { - return false - } - if !reflect.DeepEqual(bx, by) { - return false - } - return true + assert.NoError(t, err) + assert.Equal(t, bx, by) } diff --git a/internal/ulimit/ulimit.go b/internal/ulimit/ulimit.go new file mode 100644 index 0000000..4a37ee7 --- /dev/null +++ b/internal/ulimit/ulimit.go @@ -0,0 +1,7 @@ +package ulimit + +var ( + SetMax = func() error { + return nil + } +) diff --git a/internal/ulimit/ulimit_unix.go b/internal/ulimit/ulimit_unix.go new file mode 100644 index 0000000..c911a44 --- /dev/null +++ b/internal/ulimit/ulimit_unix.go @@ -0,0 +1,23 @@ +package ulimit + +import ( + "syscall" +) + +func init() { + SetMax = setMax +} + +func setMax() error { + rLimit := syscall.Rlimit{} + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return err + } + rLimit.Cur = rLimit.Max + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) + if err != nil { + return err + } + return nil +} diff --git a/main.go b/main.go index 4ef53cd..527c942 100644 --- a/main.go +++ b/main.go @@ -6,172 +6,229 @@ import ( "fmt" "os" "os/signal" - "path" - "strings" + "runtime/debug" "syscall" + "time" - "github.com/spf13/viper" "golang.org/x/sync/errgroup" "github.com/zitryss/aye-and-nay/delivery/http" + "github.com/zitryss/aye-and-nay/domain/domain" "github.com/zitryss/aye-and-nay/domain/service" "github.com/zitryss/aye-and-nay/infrastructure/cache" "github.com/zitryss/aye-and-nay/infrastructure/compressor" "github.com/zitryss/aye-and-nay/infrastructure/database" "github.com/zitryss/aye-and-nay/infrastructure/storage" + "github.com/zitryss/aye-and-nay/internal/config" + "github.com/zitryss/aye-and-nay/internal/gctuner" + "github.com/zitryss/aye-and-nay/internal/log" + "github.com/zitryss/aye-and-nay/internal/ulimit" "github.com/zitryss/aye-and-nay/pkg/errors" - "github.com/zitryss/aye-and-nay/pkg/log" -) - -var ( - ballast []byte ) func main() { - conf := "" - flag.StringVar(&conf, "config", "./config.yml", "relative filepath to a config file") - flag.Parse() - dir, file := path.Split(conf) - base := strings.TrimSuffix(file, path.Ext(file)) - - viper.SetConfigName(base) - viper.AddConfigPath(dir) - err := viper.ReadInConfig() + err := ulimit.SetMax() if err != nil { _, _ = fmt.Fprintln(os.Stderr, "critical:", err) os.Exit(1) } - ballast = make([]byte, viper.GetInt64("app.ballast")) + path := "" + flag.StringVar(&path, "config", "./config.env", "filepath to a config file") + flag.Parse() - lvl := viper.GetString("app.log") - log.SetOutput(os.Stderr) - log.SetLevel(lvl) - log.Info("logging initialized") + cach := domain.Cacher(nil) + comp := domain.Compresser(nil) + data := domain.Databaser(nil) + stor := domain.Storager(nil) - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() + reload := true + for reload { + reload = false - cach, err := cache.New(viper.GetString("cache.use")) - if err != nil { - log.Critical(err) - os.Exit(1) - } + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - comp, err := compressor.New(viper.GetString("compressor.use")) - if err != nil { - log.Critical(err) - os.Exit(1) - } + conf, err := config.New(path) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "critical:", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - data, err := database.New(viper.GetString("database.use")) - if err != nil { - log.Critical(err) - os.Exit(1) - } + if conf.Reload { + conf.OnChange(ctx, func() { + reload = true + stop() + }) + } - stor, err := storage.New(viper.GetString("storage.use")) - if err != nil { - log.Critical(err) - os.Exit(1) - } + err = log.New(conf.App.Log, conf.App.Name) + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, "critical:", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - qCalc := service.NewQueueCalc(cach) - qCalc.Monitor(ctx) + log.Info(context.Background(), "logging initialized", "log level", conf.App.Log) + + if conf.App.GcTuner == "custom" { + err = gctuner.Start(ctx, conf.App.MemTotal, conf.App.MemLimitRatio) + if err != nil { + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } + } else if conf.App.GcTuner == "go" { + debug.SetMemoryLimit(int64(float64(conf.App.MemTotal) * conf.App.MemLimitRatio)) + } - qComp := &service.QueueComp{} - if viper.GetString("compressor.use") != "mock" { - qComp = service.NewQueueComp(cach) - qComp.Monitor(ctx) - } + cach, err = cache.New(ctx, conf.Cache) + if err != nil { + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - qDel := service.NewQueueDel(cach) - qDel.Monitor(ctx) + comp, err = compressor.New(ctx, conf.Compressor) + if err != nil { + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - serv := service.New(comp, stor, data, cach, qCalc, qComp, qDel) - err = serv.CleanUp(ctx) - if err != nil { - log.Critical(err) - os.Exit(1) - } + data, err = database.New(ctx, conf.Database) + if err != nil { + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - gCalc, ctxCalc := errgroup.WithContext(ctx) - log.Info("starting calculation worker pool") - serv.StartWorkingPoolCalc(ctxCalc, gCalc) + stor, err = storage.New(ctx, conf.Storage) + if err != nil { + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - gComp := (*errgroup.Group)(nil) - ctxComp := context.Context(nil) - if viper.GetString("compressor.use") != "mock" { - gComp, ctxComp = errgroup.WithContext(ctx) - log.Info("starting compression worker pool") - serv.StartWorkingPoolComp(ctxComp, gComp) - } + qCalc := service.NewQueueCalc(cach) + qCalc.Monitor(ctx) - gDel, ctxDel := errgroup.WithContext(ctx) - log.Info("starting deletion worker pool") - serv.StartWorkingPoolDel(ctxDel, gDel) + qComp := &service.QueueComp{} + if !conf.Compressor.IsMock() { + qComp = service.NewQueueComp(cach) + qComp.Monitor(ctx) + } - middle := http.NewMiddleware(cach) - srvWait := make(chan error, 1) - srv, err := http.NewServer(middle.Chain, serv, srvWait) - if err != nil { - log.Critical(err) - os.Exit(1) - } - srv.Monitor(ctx) - log.Info("starting web server") - err = srv.Start() + qDel := service.NewQueueDel(cach) + qDel.Monitor(ctx) - log.Info("stopping web server") - if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Error(err) - } - err = <-srvWait - if err != nil { - log.Error(err) - } + serv := service.New(conf.Service, comp, stor, data, cach, qCalc, qComp, qDel) + err = serv.CleanUp(ctx) + if err != nil { + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue + } - log.Info("stopping deletion worker pool") - err = gDel.Wait() - if err != nil { - log.Error(err) - } + gCalc, ctxCalc := errgroup.WithContext(ctx) + log.Info(context.Background(), "starting calculation worker pool") + serv.StartWorkingPoolCalc(ctxCalc, gCalc) + + gComp := (*errgroup.Group)(nil) + ctxComp := context.Context(nil) + if !conf.Compressor.IsMock() { + gComp, ctxComp = errgroup.WithContext(ctx) + log.Info(context.Background(), "starting compression worker pool") + serv.StartWorkingPoolComp(ctxComp, gComp) + } + + gDel, ctxDel := errgroup.WithContext(ctx) + log.Info(context.Background(), "starting deletion worker pool") + serv.StartWorkingPoolDel(ctxDel, gDel) - if viper.GetString("compressor.use") != "mock" { - log.Info("stopping compression worker pool") - err = gComp.Wait() + middle := http.NewMiddleware(conf.Middleware, cach) + srvWait := make(chan error, 1) + srv, err := http.NewServer(conf.Server, middle.Chain, serv, srvWait) if err != nil { - log.Error(err) + log.Critical(context.Background(), "err", "stacktrace", err) + reload = true + stop() + time.Sleep(2 * time.Second) + continue } - } + srv.Monitor(ctx) + log.Info(context.Background(), "starting web server") + err = srv.Start() - log.Info("stopping calculation worker pool") - err = gCalc.Wait() - if err != nil { - log.Error(err) - } + log.Info(context.Background(), "stopping web server") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Error(context.Background(), "err", "stacktrace", err) + } + err = <-srvWait + if err != nil { + log.Error(context.Background(), "err", "stacktrace", err) + } - r, ok := cach.(*cache.Redis) - if ok { - err = r.Close() + log.Info(context.Background(), "stopping deletion worker pool") + err = gDel.Wait() if err != nil { - log.Error(err) + log.Error(context.Background(), "err", "stacktrace", err) + } + + if !conf.Compressor.IsMock() { + log.Info(context.Background(), "stopping compression worker pool") + err = gComp.Wait() + if err != nil { + log.Error(context.Background(), "err", "stacktrace", err) + } + } + + log.Info(context.Background(), "stopping calculation worker pool") + err = gCalc.Wait() + if err != nil { + log.Error(context.Background(), "err", "stacktrace", err) + } + + stop() + + b, ok := data.(*database.Badger) + if ok { + err = b.Close(context.Background()) + if err != nil { + log.Error(context.Background(), "err", "stacktrace", err) + } } } - m, ok := data.(*database.Mongo) + r, ok := cach.(*cache.Redis) if ok { - err = m.Close() + err = r.Close(context.Background()) if err != nil { - log.Error(err) + log.Error(context.Background(), "err", "stacktrace", err) } } - b, ok := data.(*database.Badger) + m, ok := data.(*database.Mongo) if ok { - err = b.Close() + err = m.Close(context.Background()) if err != nil { - log.Error(err) + log.Error(context.Background(), "err", "stacktrace", err) } } } diff --git a/pkg/base64/base64.go b/pkg/base64/base64.go index 799fd4c..bee0414 100644 --- a/pkg/base64/base64.go +++ b/pkg/base64/base64.go @@ -1,8 +1,10 @@ package base64 import ( - "encoding/base64" "encoding/binary" + "errors" + + "github.com/segmentio/asm/base64" ) func FromUint64(u uint64) string { @@ -16,5 +18,8 @@ func ToUint64(s string) (uint64, error) { if err != nil { return 0x0, err } + if len(b) != 8 { + return 0x0, errors.New("invalid input") + } return binary.LittleEndian.Uint64(b), nil } diff --git a/pkg/base64/base64_test.go b/pkg/base64/base64_test.go new file mode 100644 index 0000000..a3ac34d --- /dev/null +++ b/pkg/base64/base64_test.go @@ -0,0 +1,135 @@ +package base64 + +import ( + "flag" + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestFromUint64(t *testing.T) { + if !*unit { + t.Skip() + } + tests := []struct { + u uint64 + want string + }{ + { + u: 0x0, + want: "AAAAAAAAAAA", + }, + { + u: 0x1, + want: "AQAAAAAAAAA", + }, + { + u: math.MaxUint64, + want: "__________8", + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got := FromUint64(tt.u) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestToUint64(t *testing.T) { + if !*unit { + t.Skip() + } + tests := []struct { + s string + want uint64 + wantErr bool + }{ + { + s: "AAAAAAAAAAA", + want: 0x0, + wantErr: false, + }, + { + s: "AQAAAAAAAAA", + want: 0x1, + wantErr: false, + }, + { + s: "__________8", + want: math.MaxUint64, + wantErr: false, + }, + { + s: "00000000000", + want: 0x4dd3344dd3344dd3, + wantErr: false, + }, + { + s: "00000000001", + want: 0x4dd3344dd3344dd3, + wantErr: false, + }, + { + s: "", + want: 0x0, + wantErr: true, + }, + { + s: "A", + want: 0x0, + wantErr: true, + }, + { + s: "!@#$%^&*()_", + want: 0x0, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + got, err := ToUint64(tt.s) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +// go test -fuzz=FuzzFromUint64 -fuzztime 5s +func FuzzFromUint64(f *testing.F) { + f.Add(uint64(0)) + f.Fuzz(func(t *testing.T, u1 uint64) { + if !*unit { + t.Skip() + } + b64 := FromUint64(u1) + u2, err := ToUint64(b64) + assert.NoError(t, err) + assert.Equal(t, u1, u2) + }) +} + +// go test -fuzz=FuzzToUint64 -fuzztime 5s +func FuzzToUint64(f *testing.F) { + f.Add("AAAAAAAAAAA") + f.Fuzz(func(t *testing.T, s string) { + if !*unit { + t.Skip() + } + _, err := ToUint64(s) + if err != nil { + t.Skip() + } + }) +} diff --git a/pkg/debug/debug_test.go b/pkg/debug/debug_test.go new file mode 100644 index 0000000..f460920 --- /dev/null +++ b/pkg/debug/debug_test.go @@ -0,0 +1,39 @@ +package debug + +import ( + "errors" + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestAssert(t *testing.T) { + if !*unit { + t.Skip() + } + assert.NotPanics(t, func() { + Assert(true) + }) + assert.Panics(t, func() { + Assert(false) + }) +} + +func TestCheck(t *testing.T) { + if !*unit { + t.Skip() + } + assert.NotPanics(t, func() { + Check(nil) + }) + assert.Panics(t, func() { + Check(errors.New("")) + }) +} diff --git a/pkg/env/env.go b/pkg/env/env.go deleted file mode 100644 index fc2d867..0000000 --- a/pkg/env/env.go +++ /dev/null @@ -1,17 +0,0 @@ -package env - -import ( - "errors" - "os" -) - -func Lookup(key string) (string, error) { - val, ok := os.LookupEnv(key) - if !ok { - return "", errors.New("environment variable " + key + " not found") - } - if val == "" { - return "", errors.New("environment variable " + key + " is empty") - } - return val, nil -} diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 4a9db63..4401192 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -22,7 +22,7 @@ func Wrap(err error) error { return fmt.Errorf("%s:%d: %w", funcName, line, err) } -func Wrapf(err error, format string, args ...interface{}) error { +func Wrapf(err error, format string, args ...any) error { if err == nil { return err } @@ -43,7 +43,7 @@ func Is(err error, target error) bool { return errors.Is(err, target) } -func As(err error, target interface{}) bool { +func As(err error, target any) bool { return errors.As(err, target) } diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go index c4c5806..112c0a5 100644 --- a/pkg/errors/errors_test.go +++ b/pkg/errors/errors_test.go @@ -1,16 +1,25 @@ -//go:build unit - package errors_test import ( + "flag" "fmt" - "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/zitryss/aye-and-nay/pkg/errors" ) +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + func TestCause(t *testing.T) { + if !*unit { + t.Skip() + } tests := []struct { give error want error @@ -56,12 +65,13 @@ func TestCause(t *testing.T) { want: errors.New("1"), }, } + t.Parallel() for _, tt := range tests { + tt := tt t.Run("", func(t *testing.T) { + t.Parallel() got := errors.Cause(tt.give) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("Cause(%v) = %v, want %v", tt.give, got, tt.want) - } + assert.Equal(t, tt.want, got) }) } } diff --git a/pkg/linalg/linalg_test.go b/pkg/linalg/linalg_test.go index a929683..6181735 100644 --- a/pkg/linalg/linalg_test.go +++ b/pkg/linalg/linalg_test.go @@ -1,17 +1,28 @@ -//go:build unit - package linalg_test import ( + "flag" "fmt" "math/rand" "testing" + "github.com/stretchr/testify/assert" + . "github.com/zitryss/aye-and-nay/internal/testing" "github.com/zitryss/aye-and-nay/pkg/linalg" ) +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + func TestPageRank(t *testing.T) { + if !*unit { + t.Skip() + } + t.Parallel() edgs := map[uint64]map[uint64]int{} edgs[0x5B92] = map[uint64]int{} edgs[0x804F] = map[uint64]int{} @@ -35,9 +46,7 @@ func TestPageRank(t *testing.T) { want[0xFB26] = 0.11761540730647063 want[0xF523] = 0.07719901505201851 want[0xFC63] = 0.055433125751816706 - if !EqualMap(got, want) { - t.Error("!equalMap(got, want)") - } + assert.InDeltaMapValues(t, want, got, TOLERANCE) } func BenchmarkPageRank(b *testing.B) { diff --git a/pkg/log/log.go b/pkg/log/log.go index a1e8bc9..5147463 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -11,11 +11,11 @@ import ( type Level int const ( - ldisabled Level = iota - Ldebug // debug - Linfo // info - Lerror // error - Lcritical // critical + disabled Level = iota + DEBUG // debug + INFO // info + ERROR // error + CRITICAL // critical ) var ( @@ -32,19 +32,21 @@ func SetOutput(w io.Writer) { } func SetPrefix(prefix string) { - l.SetPrefix(prefix) + if len(prefix) > 0 { + l.SetPrefix(prefix + ": ") + } } func SetFlags(flag int) { l.SetFlags(flag) } -func SetLevel(lvl interface{}) { +func SetLevel(lvl any) { oldLevel := l.lvl - newLevel := ldisabled + newLevel := disabled switch v := lvl.(type) { case Level: - if Ldebug <= v && v <= Lcritical { + if DEBUG <= v && v <= CRITICAL { newLevel = v } else { newLevel = oldLevel @@ -53,13 +55,13 @@ func SetLevel(lvl interface{}) { v = strings.ToLower(v) switch v { case "debug": - newLevel = Ldebug + newLevel = DEBUG case "info": - newLevel = Linfo + newLevel = INFO case "error": - newLevel = Lerror + newLevel = ERROR case "critical": - newLevel = Lcritical + newLevel = CRITICAL default: newLevel = oldLevel } @@ -69,46 +71,46 @@ func SetLevel(lvl interface{}) { l.lvl = newLevel } -func Println(level Level, v ...interface{}) { - if Ldebug <= level && level <= Lcritical && l.lvl <= level { - l.Println(append([]interface{}{fmt.Sprint(level) + ":"}, v...)...) +func Println(level Level, v ...any) { + if DEBUG <= level && level <= CRITICAL && l.lvl <= level { + l.Println(append([]any{fmt.Sprint(level) + ":"}, v...)...) } } -func Printf(level Level, format string, v ...interface{}) { - if Ldebug <= level && level <= Lcritical && l.lvl <= level { - l.Printf("%s: "+format, append([]interface{}{level}, v...)...) +func Printf(level Level, format string, v ...any) { + if DEBUG <= level && level <= CRITICAL && l.lvl <= level { + l.Printf("%s: "+format, append([]any{level}, v...)...) } } -func Debug(v ...interface{}) { - Println(Ldebug, v...) +func Debug(v ...any) { + Println(DEBUG, v...) } -func Debugf(format string, v ...interface{}) { - Printf(Ldebug, format, v...) +func Debugf(format string, v ...any) { + Printf(DEBUG, format, v...) } -func Info(v ...interface{}) { - Println(Linfo, v...) +func Info(v ...any) { + Println(INFO, v...) } -func Infof(format string, v ...interface{}) { - Printf(Linfo, format, v...) +func Infof(format string, v ...any) { + Printf(INFO, format, v...) } -func Error(v ...interface{}) { - Println(Lerror, v...) +func Error(v ...any) { + Println(ERROR, v...) } -func Errorf(format string, v ...interface{}) { - Printf(Lerror, format, v...) +func Errorf(format string, v ...any) { + Printf(ERROR, format, v...) } -func Critical(v ...interface{}) { - Println(Lcritical, v...) +func Critical(v ...any) { + Println(CRITICAL, v...) } -func Criticalf(format string, v ...interface{}) { - Printf(Lcritical, format, v...) +func Criticalf(format string, v ...any) { + Printf(CRITICAL, format, v...) } diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go index c12883b..485e950 100644 --- a/pkg/log/log_test.go +++ b/pkg/log/log_test.go @@ -1,17 +1,27 @@ -//go:build unit - package log_test import ( + "flag" "strings" "testing" + "github.com/stretchr/testify/assert" + "github.com/zitryss/aye-and-nay/pkg/log" ) +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + func TestLogLevelPositive(t *testing.T) { + if !*unit { + t.Skip() + } tests := []struct { - level interface{} + level any want string }{ { @@ -31,19 +41,19 @@ func TestLogLevelPositive(t *testing.T) { want: "critical: message7\ncritical: message8: ju iv\n", }, { - level: log.Ldebug, + level: log.DEBUG, want: "debug: message1\ndebug: message2: 60 95\ninfo: message3\ninfo: message4: mx 12\nerror: message5\nerror: message6: 80 dq\ncritical: message7\ncritical: message8: ju iv\n", }, { - level: log.Linfo, + level: log.INFO, want: "info: message3\ninfo: message4: mx 12\nerror: message5\nerror: message6: 80 dq\ncritical: message7\ncritical: message8: ju iv\n", }, { - level: log.Lerror, + level: log.ERROR, want: "error: message5\nerror: message6: 80 dq\ncritical: message7\ncritical: message8: ju iv\n", }, { - level: log.Lcritical, + level: log.CRITICAL, want: "critical: message7\ncritical: message8: ju iv\n", }, } @@ -63,16 +73,17 @@ func TestLogLevelPositive(t *testing.T) { log.Critical("message7") log.Criticalf("message8: %s %s", "ju", "iv") got := w.String() - if got != tt.want { - t.Errorf("level = %v; got %v; want %v", tt.level, got, tt.want) - } + assert.Equal(t, tt.want, got) }) } } func TestLogLevelNegative(t *testing.T) { + if !*unit { + t.Skip() + } tests := []struct { - level interface{} + level any want string }{ { @@ -94,7 +105,7 @@ func TestLogLevelNegative(t *testing.T) { log.SetOutput(&w) log.SetPrefix("") log.SetFlags(0) - log.SetLevel(log.Ldebug) + log.SetLevel(log.DEBUG) log.Debug("message1") log.Debugf("message2: %d %d", 60, 95) log.Info("message3") @@ -105,9 +116,7 @@ func TestLogLevelNegative(t *testing.T) { log.Critical("message7") log.Criticalf("message8: %s %s", "ju", "iv") got := w.String() - if got != tt.want { - t.Errorf("level = %v; got %v; want %v", log.Ldebug, got, tt.want) - } + assert.Equal(t, tt.want, got) }) } } diff --git a/pkg/pool/pool.go b/pkg/pool/pool.go index 9dc5688..eb45695 100644 --- a/pkg/pool/pool.go +++ b/pkg/pool/pool.go @@ -7,7 +7,7 @@ import ( var ( p = &sync.Pool{ - New: func() interface{} { + New: func() any { return &bytes.Buffer{} }, } @@ -17,6 +17,18 @@ func GetBuffer() *bytes.Buffer { return p.Get().(*bytes.Buffer) } +func GetBufferN(n int64) *bytes.Buffer { + if n <= 0 { + return GetBuffer() + } + buf := GetBuffer() + delta := buf.Cap() - int(n) + if delta < 0 { + buf.Grow(-delta) + } + return buf +} + func PutBuffer(buf *bytes.Buffer) { buf.Reset() p.Put(buf) diff --git a/pkg/pool/pool_test.go b/pkg/pool/pool_test.go new file mode 100644 index 0000000..c197618 --- /dev/null +++ b/pkg/pool/pool_test.go @@ -0,0 +1,27 @@ +package pool + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestBuffer(t *testing.T) { + if !*unit { + t.Skip() + } + buf1 := GetBuffer() + assert.NotNil(t, buf1) + PutBuffer(buf1) + buf2 := GetBufferN(100) + assert.NotNil(t, buf2) + assert.GreaterOrEqual(t, buf2.Cap(), 100) + PutBuffer(buf2) +} diff --git a/pkg/rand/rand_test.go b/pkg/rand/rand_test.go new file mode 100644 index 0000000..1cd4206 --- /dev/null +++ b/pkg/rand/rand_test.go @@ -0,0 +1,23 @@ +package rand + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + +func TestId(t *testing.T) { + if !*unit { + t.Skip() + } + got, err := Id() + assert.NoError(t, err) + assert.Positive(t, got) +} diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go index b43fd84..b39c445 100644 --- a/pkg/retry/retry_test.go +++ b/pkg/retry/retry_test.go @@ -1,17 +1,26 @@ -//go:build unit - package retry_test import ( - "reflect" + "flag" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/zitryss/aye-and-nay/pkg/errors" "github.com/zitryss/aye-and-nay/pkg/retry" ) +var ( + unit = flag.Bool("unit", false, "") + integration = flag.Bool("int", false, "") + ci = flag.Bool("ci", false, "") +) + func TestDo1(t *testing.T) { + if !*unit { + t.Skip() + } type give struct { times int pause time.Duration @@ -53,25 +62,27 @@ func TestDo1(t *testing.T) { }, }, } + t.Parallel() for _, tt := range tests { + tt := tt t.Run("", func(t *testing.T) { + t.Parallel() c := 0 err := retry.Do(tt.times, tt.pause, func() error { c++ time.Sleep(tt.busy) return nil }) - if !reflect.DeepEqual(err, tt.err) { - t.Errorf("Do() err = %v, want = %v", err, tt.err) - } - if c != tt.calls { - t.Errorf("Do() calls = %v, want = %v", c, tt.calls) - } + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.calls, c) }) } } func TestDo2(t *testing.T) { + if !*unit { + t.Skip() + } type give struct { times int pause time.Duration @@ -113,25 +124,27 @@ func TestDo2(t *testing.T) { }, }, } + t.Parallel() for _, tt := range tests { + tt := tt t.Run("", func(t *testing.T) { + t.Parallel() c := 0 err := retry.Do(tt.times, tt.pause, func() error { c++ time.Sleep(tt.busy) return errors.New("no luck") }) - if !reflect.DeepEqual(err, tt.err) { - t.Errorf("Do() err = %v, want = %v", err, tt.err) - } - if c != tt.calls { - t.Errorf("Do() calls = %v, want = %v", c, tt.calls) - } + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.calls, c) }) } } func TestDo3(t *testing.T) { + if !*unit { + t.Skip() + } type give struct { times int pause time.Duration @@ -173,8 +186,11 @@ func TestDo3(t *testing.T) { }, }, } + t.Parallel() for _, tt := range tests { + tt := tt t.Run("", func(t *testing.T) { + t.Parallel() c := 0 err := retry.Do(tt.times, tt.pause, func() error { c++ @@ -184,12 +200,8 @@ func TestDo3(t *testing.T) { } return nil }) - if !reflect.DeepEqual(err, tt.err) { - t.Errorf("Do() err = %v, want = %v", err, tt.err) - } - if c != tt.calls { - t.Errorf("Do() calls = %v, want = %v", c, tt.calls) - } + assert.Equal(t, tt.err, err) + assert.Equal(t, tt.calls, c) }) } } diff --git a/pkg/unit/unit.go b/pkg/unit/unit.go deleted file mode 100644 index 10877a5..0000000 --- a/pkg/unit/unit.go +++ /dev/null @@ -1,8 +0,0 @@ -package unit - -const ( - _ int64 = 1 << (10 * iota) - KB - MB - GB -)