[hw-2] add tests, coverage, benchmarks

This commit is contained in:
Никита Шубин
2025-05-31 10:47:47 +00:00
parent 1a3d4892a8
commit a0e36639ca
22 changed files with 3058 additions and 48 deletions

3
.gitignore vendored
View File

@@ -1,2 +1,5 @@
.idea .idea
.vscode
bin bin
.coverage
allure-results

View File

@@ -13,6 +13,10 @@ output:
print-linter-name: true print-linter-name: true
linters-settings: linters-settings:
gocyclo:
min-complexity: 15
gocognit:
min-complexity: 20
govet: govet:
enable: enable:
- shadow - shadow
@@ -28,6 +32,8 @@ linters:
# - dupl - it's very slow, enable if you really know why you need it # - dupl - it's very slow, enable if you really know why you need it
- errcheck - errcheck
- goconst - goconst
- gocognit
- gocyclo
- goimports - goimports
- gosec - gosec
- govet - govet

View File

@@ -1,10 +1,27 @@
include make/lint.mk include make/lint.mk
include make/build.mk include make/build.mk
include make/coverage.mk
include make/generate.mk
PKGS = $(shell go list ./... | grep -vE 'mock|config|generated|header|document|internal/pb')
INTEGRATION_TAG = integration
lint: cart-lint loms-lint notifier-lint comments-lint lint: cart-lint loms-lint notifier-lint comments-lint
build: cart-build loms-build notifier-build comments-build build: cart-build loms-build notifier-build comments-build
coverage: cart-coverage loms-coverage notifier-coverage comments-coverage
generate: cart-generate loms-generate notifier-generate comments-generate
run-all: run-all:
echo "starting build" echo "starting build"
docker-compose up --build docker-compose up --build
integration-test:
ALLURE_OUTPUT_PATH=$(shell pwd) go test -v -tags=$(INTEGRATION_TAG) ./cart/tests/integration
bench:
go test -bench=BenchmarkInMemoryRepository -benchmem ./cart/internal/domain/cart/repository -benchtime 3s
.PHONY: lint, coverage

View File

@@ -1,8 +1,14 @@
BINDIR=${CURDIR}/bin BINDIR=${CURDIR}/bin
PACKAGE=route256/cart PACKAGE=route256/cart
PKGS = $(shell go list ./... | grep -vE 'mock|config|generated|header|document|internal/pb')
bindir: bindir:
mkdir -p ${BINDIR} mkdir -p ${BINDIR}
build: bindir build: bindir
echo "build cart" echo "build cart"
coverage:
@go test -race -coverprofile=coverage.txt -covermode=atomic $(PKGS)
@go tool cover -html=coverage.txt -o coverage.html

View File

@@ -1,25 +1,79 @@
module route256/cart module route256/cart
go 1.23.1 go 1.23.9
require ( require (
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.0.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.33.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
golang.org/x/net v0.34.0 // indirect github.com/magiconair/properties v1.8.10 // indirect
golang.org/x/text v0.22.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/ozontech/allure-go/pkg/allure v0.6.14 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
google.golang.org/protobuf v1.36.6 // indirect
) )
require ( require (
github.com/go-playground/validator/v10 v10.26.0 github.com/go-playground/validator/v10 v10.26.0
github.com/gojuno/minimock/v3 v3.4.5
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
golang.org/x/sys v0.30.0 // indirect github.com/ozontech/allure-go/pkg/framework v0.6.34
github.com/testcontainers/testcontainers-go v0.37.0
golang.org/x/sys v0.33.0 // indirect
) )

View File

@@ -1,6 +1,48 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
@@ -8,30 +50,166 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/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/gojuno/minimock/v3 v3.4.5 h1:Jcb0tEYZvVlQNtAAYpg3jCOoSwss2c1/rNugYTzj304=
github.com/gojuno/minimock/v3 v3.4.5/go.mod h1:o9F8i2IT8v3yirA7mmdpNGzh1WNesm6iQakMtQV6KiE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
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.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
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.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/ozontech/allure-go/pkg/allure v0.6.14 h1:lDamtSF+WtHQLg2+qQYijtC4Fk3KLGb6txNxxTZwUGc=
github.com/ozontech/allure-go/pkg/allure v0.6.14/go.mod h1:4oEG2yq+DGOzJS/ZjPc87C/mx3tAnlYpYonk77Ru/vQ=
github.com/ozontech/allure-go/pkg/framework v0.6.34 h1:IjM65Y3JP7ale7Ug3aBnFV4+c1NYYBCrgl/VrtEd/FY=
github.com/ozontech/allure-go/pkg/framework v0.6.34/go.mod h1:oISDLE6Tfww35TBQz+1nrtbLtyBqR6ELxOtJ+MVjHOw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg=
github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE=
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=

View File

@@ -52,7 +52,7 @@ func NewApp(configPath string) (*App, error) {
app := &App{ app := &App{
config: c, config: c,
} }
app.server.Handler = app.bootstrapHandlers() app.server.Handler = app.BootstrapHandlers(app.setupCartService())
return app, nil return app, nil
} }
@@ -70,7 +70,7 @@ func (app *App) ListenAndServe() error {
return app.server.Serve(l) return app.server.Serve(l)
} }
func (app *App) bootstrapHandlers() http.Handler { func (app *App) setupCartService() *service.CartService {
transport := http.DefaultTransport transport := http.DefaultTransport
transport = round_trippers.NewLogRoundTripper(transport) transport = round_trippers.NewLogRoundTripper(transport)
transport = round_trippers.NewRetryRoundTripper(transport, productsRetryAttemptsDefault, productsInitialDelaySecDefault) transport = round_trippers.NewRetryRoundTripper(transport, productsRetryAttemptsDefault, productsInitialDelaySecDefault)
@@ -88,8 +88,11 @@ func (app *App) bootstrapHandlers() http.Handler {
const userCartCap = 100 const userCartCap = 100
cartRepository := repository.NewInMemoryRepository(userCartCap) cartRepository := repository.NewInMemoryRepository(userCartCap)
cartService := service.NewCartService(cartRepository, productService)
return service.NewCartService(cartRepository, productService)
}
func (app *App) BootstrapHandlers(cartService *service.CartService) http.Handler {
s := server.NewServer(cartService) s := server.NewServer(cartService)
mx := http.NewServeMux() mx := http.NewServeMux()

View File

@@ -42,7 +42,7 @@ func (s *Server) GetItemsByUserIDHandler(w http.ResponseWriter, r *http.Request)
if err != nil { if err != nil {
makeErrorResponse(w, err, http.StatusNotFound) makeErrorResponse(w, err, http.StatusNotFound)
log.Trace().Err(err).Msgf("cartService.GetItemsByUserID: %d", http.StatusInternalServerError) log.Trace().Err(err).Msgf("cartService.GetItemsByUserID: %d", http.StatusNotFound)
return return
} }

View File

@@ -0,0 +1,59 @@
package repository
import (
"context"
"testing"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
)
var (
ctx = context.Background()
testItem = &model.Item{
Product: &model.Product{
Sku: 1,
Name: "bench",
Price: 100,
},
Count: 1,
}
)
func BenchmarkInMemoryRepository_AddItem(b *testing.B) {
repo := NewInMemoryRepository(1024)
b.ResetTimer()
for i := 1; i < b.N; i++ {
uid := entity.UID(i % 1000000)
_ = repo.AddItem(ctx, uid, testItem)
}
}
func BenchmarkInMemoryRepository_AddItemParallel(b *testing.B) {
repo := NewInMemoryRepository(1024)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
var i int
for pb.Next() {
uid := entity.UID(i % 1000000)
_ = repo.AddItem(ctx, uid, testItem)
i++
}
})
}
func BenchmarkInMemoryRepository_DeleteItem(b *testing.B) {
repo := NewInMemoryRepository(1024)
for i := 1; i < 1_000; i++ {
_ = repo.AddItem(ctx, entity.UID(i), testItem)
}
b.ResetTimer()
for i := 1; i < b.N; i++ {
uid := entity.UID(i % 1000000)
_ = repo.DeleteItem(ctx, uid, entity.Sku(testItem.Product.Sku))
}
}

View File

@@ -0,0 +1,269 @@
package repository
import (
"context"
"testing"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAddItem(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
sku = entity.Sku(123)
item = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: sku,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item)
require.NoError(t, err, "check add item error")
assert.Len(t, repo.storage, 1, "check map length")
assert.Len(t, repo.storage[uid].Items, 1, "check list length")
assert.Equal(t, uint32(2), repo.storage[uid].ItemCount[sku], "check count value")
assert.Equal(t, uid, repo.storage[uid].UserID, "check uid")
}
func TestAddItemMultiple(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
item1 = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: 1,
},
Count: 1,
}
item2 = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: 2,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item1)
require.NoError(t, err, "check add item error")
err = repo.AddItem(ctx, uid, item2)
require.NoError(t, err, "check add item error")
assert.Len(t, repo.storage, 1, "check map length")
assert.Len(t, repo.storage[uid].Items, 2, "check list length")
assert.Equal(t, uint32(1), repo.storage[uid].ItemCount[1], "check count value of item 1")
assert.Equal(t, uint32(2), repo.storage[uid].ItemCount[2], "check count value of item 2")
assert.Equal(t, uid, repo.storage[uid].UserID, "check uid")
}
func TestGetItemsByUserID(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
item1 = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: 1,
},
Count: 1,
}
item2 = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: 2,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item1)
require.NoError(t, err, "check add item error")
err = repo.AddItem(ctx, uid, item2)
require.NoError(t, err, "check add item error")
cart, err := repo.GetItemsByUserID(ctx, uid)
require.NoError(t, err, "check get items error")
require.Equal(t, uid, cart.UserID, "check cart uid")
require.Len(t, cart.Items, 2, "check cart len")
require.Equal(t, uint32(1), cart.ItemCount[item1.Product.Sku], "check item 1 count")
require.Equal(t, uint32(2), cart.ItemCount[item2.Product.Sku], "check item 2 count")
}
func TestGetItemsByUserIDNoCart(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
ctx = context.Background()
)
cart, err := repo.GetItemsByUserID(ctx, uid)
require.NoError(t, err, "check get items error")
require.Equal(t, entity.Cart{}, cart, "check cart")
}
func TestDeleteItem(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
sku = entity.Sku(123)
item = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: sku,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item)
require.NoError(t, err, "check add item error")
assert.Len(t, repo.storage, 1, "check map length")
assert.Len(t, repo.storage[uid].Items, 1, "check list length")
err = repo.DeleteItem(ctx, uid, item.Product.Sku)
require.NoError(t, err, "check delete item error")
assert.Len(t, repo.storage[uid].Items, 0, "check list length")
}
func TestDeleteItemUnknownUser(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
sku = entity.Sku(123)
ctx = context.Background()
)
err := repo.DeleteItem(ctx, uid, sku)
require.ErrorIs(t, err, model.ErrCartNotFound, "check cart not found err")
}
func TestDeleteItemUnknownItem(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
sku = entity.Sku(123)
item = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: sku,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item)
require.NoError(t, err, "check add item error")
assert.Len(t, repo.storage, 1, "check map length")
assert.Len(t, repo.storage[uid].Items, 1, "check list length")
err = repo.DeleteItem(ctx, uid, 1)
require.ErrorIs(t, err, model.ErrItemNotFoundInCart, "check delete item error")
assert.Len(t, repo.storage[uid].Items, 1, "check list length")
}
func TestDeleteItemEmptyCart(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
sku = entity.Sku(123)
item = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: sku,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item)
require.NoError(t, err, "check add item error")
assert.Len(t, repo.storage, 1, "check map length")
assert.Len(t, repo.storage[uid].Items, 1, "check list length")
err = repo.DeleteItem(ctx, uid, item.Product.Sku)
require.NoError(t, err, "check delete item error")
assert.Len(t, repo.storage[uid].Items, 0, "check list length")
err = repo.DeleteItem(ctx, uid, item.Product.Sku)
require.NoError(t, err, "check empty cart no err")
}
func TestDeleteItemsByUserID(t *testing.T) {
t.Parallel()
var (
repo = NewInMemoryRepository(10)
uid = entity.UID(1337)
sku = entity.Sku(123)
item = &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: sku,
},
Count: 2,
}
ctx = context.Background()
)
err := repo.AddItem(ctx, uid, item)
require.NoError(t, err, "check add item error")
assert.Len(t, repo.storage, 1, "check map length")
assert.Len(t, repo.storage[uid].Items, 1, "check list length")
err = repo.DeleteItemsByUserID(ctx, uid)
require.NoError(t, err, "check delete items error")
assert.Len(t, repo.storage, 0, "check storage length")
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
//go:generate minimock -i Repository -o ./mock -s _mock.go
type Repository interface { type Repository interface {
AddItem(ctx context.Context, userID entity.UID, item *model.Item) error AddItem(ctx context.Context, userID entity.UID, item *model.Item) error
GetItemsByUserID(ctx context.Context, userID entity.UID) (entity.Cart, error) GetItemsByUserID(ctx context.Context, userID entity.UID) (entity.Cart, error)
@@ -87,7 +88,10 @@ func (s *CartService) GetItemsByUserID(ctx context.Context, userID entity.UID) (
errCh := make(chan error, 1) errCh := make(chan error, 1)
var wg sync.WaitGroup var (
wg sync.WaitGroup
sumMutex sync.Mutex
)
for idx, sku := range cart.Items { for idx, sku := range cart.Items {
wg.Add(1) wg.Add(1)
@@ -111,7 +115,10 @@ func (s *CartService) GetItemsByUserID(ctx context.Context, userID entity.UID) (
Product: product, Product: product,
Count: count, Count: count,
} }
sumMutex.Lock()
resultCart.TotalPrice += uint32(product.Price) * count resultCart.TotalPrice += uint32(product.Price) * count
sumMutex.Unlock()
}(sku, cart.ItemCount[sku], idx) }(sku, cart.ItemCount[sku], idx)
} }

View File

@@ -0,0 +1,575 @@
package service
import (
"context"
"errors"
"testing"
"github.com/gojuno/minimock/v3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"route256/cart/internal/domain/cart/service/mock"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
)
const (
validPrice = 123
validName = "some product name"
)
type ProductServiceFake struct{}
func (f *ProductServiceFake) GetProductBySku(_ context.Context, sku entity.Sku) (*model.Product, error) {
if sku%2 == 0 {
return nil, errors.New("empty shelf")
}
return &model.Product{
Name: validName,
Price: validPrice,
Sku: sku,
}, nil
}
func TestCartService_AddItem(t *testing.T) {
t.Parallel()
ctx := context.Background()
mc := minimock.NewController(t)
testSKU := entity.Sku(199)
testItem := model.Item{
Product: &model.Product{
Name: validName,
Price: 123,
Sku: testSKU,
},
Count: 1,
}
type fields struct {
repository Repository
productService ProductService
}
type args struct {
ctx context.Context
item *model.Item
userID entity.UID
}
tests := []struct {
name string
fields fields
args args
wantErr require.ErrorAssertionFunc
}{
{
name: "success",
fields: fields{
repository: mock.NewRepositoryMock(mc).
AddItemMock.
Expect(ctx, 1337, &testItem).
Return(nil),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
item: &testItem,
userID: 1337,
},
wantErr: require.NoError,
},
{
name: "invalid item",
fields: fields{
repository: nil,
productService: nil,
},
args: args{
ctx: ctx,
item: &model.Item{
Product: &model.Product{
Name: "name",
Price: 123,
Sku: 0,
},
Count: 0,
},
userID: 0,
},
wantErr: require.Error,
},
{
name: "invalid user id",
fields: fields{
repository: nil,
productService: nil,
},
args: args{
ctx: ctx,
item: &testItem,
userID: 0,
},
wantErr: require.Error,
},
{
name: "product service error",
fields: fields{
repository: nil,
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
item: &model.Item{
Product: &model.Product{
Name: "",
Price: 0,
Sku: 4,
},
Count: 1,
},
userID: 1337,
},
wantErr: require.Error,
},
{
name: "repository error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
AddItemMock.
Expect(ctx, 1337, &testItem).
Return(assert.AnError),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
item: &testItem,
userID: 1337,
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &CartService{
repository: tt.fields.repository,
productService: tt.fields.productService,
}
err := s.AddItem(tt.args.ctx, tt.args.userID, tt.args.item)
tt.wantErr(t, err, "check add review error")
})
}
}
func TestCartService_GetItemsByUserID(t *testing.T) {
t.Parallel()
ctx := context.Background()
mc := minimock.NewController(t)
testUID := entity.UID(1337)
testSKU := entity.Sku(199)
testModelCart := model.Cart{
UserID: testUID,
Items: []*model.Item{
{
Product: &model.Product{
Name: validName,
Price: validPrice,
Sku: testSKU,
},
Count: 1,
},
},
TotalPrice: validPrice,
}
testEntityCart := entity.Cart{
UserID: testUID,
Items: []entity.Sku{testSKU},
ItemCount: map[entity.Sku]uint32{testSKU: 1},
}
type fields struct {
repository Repository
productService ProductService
}
type args struct {
ctx context.Context
userID entity.UID
}
tests := []struct {
name string
fields fields
args args
want *model.Cart
wantErr require.ErrorAssertionFunc
}{
{
name: "success 1 item",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(testEntityCart, nil),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
userID: testUID,
},
want: &testModelCart,
wantErr: require.NoError,
},
{
name: "success 2 items",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{testSKU},
ItemCount: map[entity.Sku]uint32{testSKU: 2},
}, nil),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
userID: testUID,
},
want: &model.Cart{
UserID: testUID,
Items: []*model.Item{
{
Product: &model.Product{
Name: validName,
Price: validPrice,
Sku: testSKU,
},
Count: 2,
},
},
TotalPrice: validPrice * 2,
},
wantErr: require.NoError,
},
{
name: "success 2 different items",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{testSKU, 1},
ItemCount: map[entity.Sku]uint32{testSKU: 1, 1: 1},
}, nil),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
userID: testUID,
},
want: &model.Cart{
UserID: testUID,
Items: []*model.Item{
{
Product: &model.Product{
Name: validName,
Price: validPrice,
Sku: 1,
},
Count: 1,
},
{
Product: &model.Product{
Name: validName,
Price: validPrice,
Sku: testSKU,
},
Count: 1,
},
},
TotalPrice: validPrice * 2,
},
wantErr: require.NoError,
},
{
name: "invalid user id",
fields: fields{
repository: nil,
productService: nil,
},
args: args{
ctx: ctx,
userID: 0,
},
want: nil,
wantErr: require.Error,
},
{
name: "repository error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(entity.Cart{}, assert.AnError),
productService: nil,
},
args: args{
ctx: ctx,
userID: testUID,
},
want: nil,
wantErr: require.Error,
},
{
name: "empty cart",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(entity.Cart{}, nil),
productService: nil,
},
args: args{
ctx: ctx,
userID: testUID,
},
want: nil,
wantErr: require.Error,
},
{
name: "product service error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{2},
ItemCount: map[entity.Sku]uint32{2: 1},
}, nil),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
userID: testUID,
},
want: nil,
wantErr: require.Error,
},
{
name: "product service error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
GetItemsByUserIDMock.
Expect(ctx, testUID).
Return(entity.Cart{
UserID: testUID,
Items: []entity.Sku{2},
ItemCount: map[entity.Sku]uint32{2: 1},
}, nil),
productService: &ProductServiceFake{},
},
args: args{
ctx: ctx,
userID: testUID,
},
want: nil,
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &CartService{
repository: tt.fields.repository,
productService: tt.fields.productService,
}
got, err := s.GetItemsByUserID(tt.args.ctx, tt.args.userID)
tt.wantErr(t, err, "check add review error")
assert.True(t, assert.EqualExportedValues(t, tt.want, got), "got unexpected cart")
})
}
}
func TestCartService_DeleteItem(t *testing.T) {
t.Parallel()
ctx := context.Background()
mc := minimock.NewController(t)
testSKU := entity.Sku(199)
testUID := entity.UID(1337)
type fields struct {
repository Repository
}
type args struct {
ctx context.Context
userID entity.UID
sku entity.Sku
}
tests := []struct {
name string
fields fields
args args
wantErr require.ErrorAssertionFunc
}{
{
name: "success",
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemMock.
Expect(ctx, testUID, testSKU).
Return(nil),
},
args: args{
ctx: ctx,
userID: testUID,
sku: testSKU,
},
wantErr: require.NoError,
},
{
name: "invalid user id",
fields: fields{
repository: nil,
},
args: args{
ctx: ctx,
userID: 0,
sku: testSKU,
},
wantErr: require.Error,
},
{
name: "invalid sku",
fields: fields{
repository: nil,
},
args: args{
ctx: ctx,
userID: testUID,
sku: 0,
},
wantErr: require.Error,
},
{
name: "repository error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemMock.
Expect(ctx, testUID, testSKU).
Return(assert.AnError),
},
args: args{
ctx: ctx,
sku: testSKU,
userID: testUID,
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &CartService{
repository: tt.fields.repository,
}
err := s.DeleteItem(tt.args.ctx, tt.args.userID, tt.args.sku)
tt.wantErr(t, err, "check add review error")
})
}
}
func TestCartService_DeleteItemsByUserID(t *testing.T) {
t.Parallel()
ctx := context.Background()
mc := minimock.NewController(t)
testUID := entity.UID(1337)
type fields struct {
repository Repository
}
type args struct {
ctx context.Context
userID entity.UID
}
tests := []struct {
name string
fields fields
args args
wantErr require.ErrorAssertionFunc
}{
{
name: "success",
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemsByUserIDMock.
Expect(ctx, testUID).
Return(nil),
},
args: args{
ctx: ctx,
userID: testUID,
},
wantErr: require.NoError,
},
{
name: "invalid user id",
fields: fields{
repository: nil,
},
args: args{
ctx: ctx,
userID: 0,
},
wantErr: require.Error,
},
{
name: "repository error",
fields: fields{
repository: mock.NewRepositoryMock(mc).
DeleteItemsByUserIDMock.
Expect(ctx, testUID).
Return(assert.AnError),
},
args: args{
ctx: ctx,
userID: testUID,
},
wantErr: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := &CartService{
repository: tt.fields.repository,
}
err := s.DeleteItemsByUserID(tt.args.ctx, tt.args.userID)
tt.wantErr(t, err, "check add review error")
})
}
}

View File

@@ -0,0 +1,193 @@
//go:build integration
// +build integration
package integration
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/ozontech/allure-go/pkg/framework/provider"
"github.com/ozontech/allure-go/pkg/framework/suite"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"route256/cart/internal/app"
"route256/cart/internal/domain/cart/repository"
cartService "route256/cart/internal/domain/cart/service"
"route256/cart/internal/domain/entity"
"route256/cart/internal/domain/model"
productsService "route256/cart/internal/domain/products/service"
)
const (
productServiceImage = "gitlab-registry.ozon.dev/go/classroom-18/students/base/products:latest"
testToken = "testToken"
testSku = 1076963
testName = "Теория нравственных чувств | Смит Адам"
testUID = entity.UID(1337)
)
type CartHandlerSuite struct {
suite.Suite
psContainer testcontainers.Container
server *httptest.Server
cartSvc *cartService.CartService
}
func TestCartHandlerSuite(t *testing.T) {
suite.RunSuite(t, new(CartHandlerSuite))
}
func (s *CartHandlerSuite) BeforeAll(t provider.T) {
ctx := context.Background()
t.WithNewStep("start product-service container", func(sCtx provider.StepCtx) {
req := testcontainers.ContainerRequest{
Image: productServiceImage,
ExposedPorts: []string{"8082/tcp"},
WaitingFor: wait.ForHTTP("/docs").WithStartupTimeout(10 * time.Second),
}
container, err := testcontainers.GenericContainer(ctx,
testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
sCtx.Require().NoError(err, "create container")
s.psContainer = container
})
var productURL string
t.WithNewStep("discover product-service URL", func(sCtx provider.StepCtx) {
endpoint, err := s.psContainer.Endpoint(ctx, "")
sCtx.Require().NoError(err)
productURL = endpoint
})
t.WithNewStep("init cart-service", func(sCtx provider.StepCtx) {
prodClient := productsService.NewProductService(
*http.DefaultClient,
testToken,
productURL,
)
repo := repository.NewInMemoryRepository(10)
s.cartSvc = cartService.NewCartService(repo, prodClient)
appSrv := &app.App{}
s.server = httptest.NewServer(appSrv.BootstrapHandlers(s.cartSvc))
})
}
func (s *CartHandlerSuite) AfterAll(t provider.T) {
_ = s.psContainer.Terminate(context.Background())
s.server.Close()
}
// DELETE /user/<user_id>/cart/<sku_id>
func (s *CartHandlerSuite) TestDeleteItemPositive(t provider.T) {
ctx := context.Background()
item := &model.Item{
Product: &model.Product{
Sku: testSku,
},
Count: 1,
}
t.WithNewStep("fill cart in usecase", func(sCtx provider.StepCtx) {
err := s.cartSvc.AddItem(context.Background(),
testUID, item)
sCtx.Require().NoError(err)
cart, err := s.cartSvc.GetItemsByUserID(ctx, testUID)
sCtx.Require().NoError(err)
sCtx.Require().Len(cart.Items, 1, "check cart contains product")
sCtx.Require().Equal(testUID, cart.UserID, "check user ID equals")
sCtx.Require().Equal(item.Product.Sku, cart.Items[0].Product.Sku, "check product sku equals")
})
t.WithNewStep("call DELETE handler", func(sCtx provider.StepCtx) {
url := fmt.Sprintf("%s/user/%d/cart/%d", s.server.URL, testUID, testSku)
req, err := http.NewRequest(http.MethodDelete, url, nil)
sCtx.Require().NoError(err)
resp, err := s.server.Client().Do(req)
sCtx.Require().NoError(err)
defer resp.Body.Close()
sCtx.Require().Equal(http.StatusNoContent, resp.StatusCode, "delete handler unexpected status code")
})
t.WithNewStep("check cart empty in usecase", func(sCtx provider.StepCtx) {
_, err := s.cartSvc.GetItemsByUserID(ctx, testUID)
sCtx.Require().Error(err)
})
}
// GET /user/<user_id>/cart
func (s *CartHandlerSuite) TestGetCartPositive(t provider.T) {
ctx := context.Background()
item := &model.Item{
Product: &model.Product{
Sku: testSku,
},
Count: 2,
}
t.WithNewStep("fill cart in usecase", func(sCtx provider.StepCtx) {
err := s.cartSvc.AddItem(context.Background(),
testUID, item)
sCtx.Require().NoError(err)
cart, err := s.cartSvc.GetItemsByUserID(ctx, testUID)
sCtx.Require().NoError(err)
sCtx.Require().Len(cart.Items, 1, "check cart contains product")
sCtx.Require().Equal(testUID, cart.UserID, "check user ID equals")
sCtx.Require().Equal(item.Product.Sku, cart.Items[0].Product.Sku, "check product sku equals")
sCtx.Require().Equal(item.Count, cart.Items[0].Count, "check product count")
})
t.WithNewStep("call GET handler", func(sCtx provider.StepCtx) {
url := fmt.Sprintf("%s/user/%d/cart", s.server.URL, testUID)
req, err := http.NewRequest(http.MethodGet, url, nil)
sCtx.Require().NoError(err)
resp, err := s.server.Client().Do(req)
sCtx.Require().NoError(err)
defer resp.Body.Close()
sCtx.Require().Equal(http.StatusOK, resp.StatusCode, "check GET status code")
sCtx.Require().Equal("application/json", resp.Header.Get("Content-Type"), "check GET content type")
var body struct {
Items []struct {
Sku int64 `json:"sku"`
Name string `json:"name"`
Count uint32 `json:"count"`
Price uint32 `json:"price"`
} `json:"items"`
TotalPrice uint32 `json:"total_price"`
}
sCtx.Require().NoError(json.NewDecoder(resp.Body).Decode(&body))
sCtx.Require().Len(body.Items, 1, "check items len")
sCtx.Require().Equal(int64(testSku), body.Items[0].Sku, "check sku")
sCtx.Require().Equal(uint32(2), body.Items[0].Count, "check item count")
sCtx.Require().Greater(body.TotalPrice, uint32(0), "check price")
})
}

View File

@@ -2,7 +2,7 @@
## Введение ## Введение
В рамках учебного проекта предстоит реализовать систему, состоящую из нескольких сервисов, которая В рамках учебного проекта предстоит реализовать систему, состоящую из нескольких сервисов, которая
будет моделировать работу простого интернет магазина включая такие бизнес процессы как: будет моделировать работу простого интернет магазина включая такие бизнес процессы как:
- добавление товаров в корзину и их удаление из нее - добавление товаров в корзину и их удаление из нее
@@ -15,4 +15,4 @@
## Содержание ## Содержание
1. [Основы Go](./homework-1) 1. [Основы Go](./homework-1)
2. [Тестирование в Go](./homework-2)

View File

@@ -4,13 +4,13 @@
## Основное задание ## Основное задание
Необходимо имплементировать сервис, отвечающий за работу с корзиной пользователя (сервис Cart). Логика работы методов Необходимо имплементировать сервис, отвечающий за работу с корзиной пользователя (сервис Cart). Логика работы методов
и их контракты описаны ниже. и их контракты описаны ниже.
Требования к решению: Требования к решению:
1. Используем HTTP, на основе стандартной библиотеки Go 1.23 1. Используем HTTP, на основе стандартной библиотеки Go 1.23
2. Для определения существования товара делаем поход в сервис `products` 2. Для определения существования товара делаем поход в сервис `product-service`
3. Состояние храним в in-memory, персистентное хранилище на данный момент не требуется 3. Состояние храним в in-memory, персистентное хранилище на данный момент не требуется
4. Никакого резерва стоков не делаем, логика простейшая 4. Никакого резерва стоков не делаем, логика простейшая
@@ -19,13 +19,13 @@
1. Делаем Middleware, который будет логировать поступающие запросы 1. Делаем Middleware, который будет логировать поступающие запросы
2. Делаем валидацию входящих структур на основе любой Open Source библиотеки (можно подсмотреть 2. Делаем валидацию входящих структур на основе любой Open Source библиотеки (можно подсмотреть
тут - https://awesome-go.com/validation/) тут - https://awesome-go.com/validation/)
3. Делаем ретраи в `products` на 420/429 статус в виде Client Middleware. 3 ретрая, потом ошибка 3. Делаем ретраи в `product-service` на 420/429 статус в виде Client Middleware. 3 ретрая, потом ошибка
## Спецификация ## Спецификация
### Добавить товар в корзину ### Добавить товар в корзину
Идентификатором товара является числовой идентификатор SKU. Метод добавляет указанный товар в корзину Идентификатором товара является числовой идентификатор SKU. Метод добавляет указанный товар в корзину
определенного пользователя. Каждый пользователь имеет числовой идентификатор userID. При добавлении в корзину определенного пользователя. Каждый пользователь имеет числовой идентификатор userID. При добавлении в корзину
проверяем, что товар существует в специальном сервисе. проверяем, что товар существует в специальном сервисе.
@@ -54,7 +54,7 @@
| Добавление валидного SKU для пользователя (user_id) с нулевым или отрицательным значением | 400 | Идентификатор пользователя должен быть натуральным числом (больше нуля) | | Добавление валидного SKU для пользователя (user_id) с нулевым или отрицательным значением | 400 | Идентификатор пользователя должен быть натуральным числом (больше нуля) |
| Добавление SKU с нулевым или отрицательным значением | 400 | SKU должен быть натуральным числом (больше нуля) | | Добавление SKU с нулевым или отрицательным значением | 400 | SKU должен быть натуральным числом (больше нуля) |
| Добавление SKU с нулевым или отрицательным количеством (count) | 400 | Количество должно быть натуральным числом (больше нуля) | | Добавление SKU с нулевым или отрицательным количеством (count) | 400 | Количество должно быть натуральным числом (больше нуля) |
| Добавление несуществующего SKU в корзину | 412 | SKU должен существовать в сервисе `products` | | Добавление несуществующего SKU в корзину | 412 | SKU должен существовать в сервисе `product-service` |
**Диаграмма последовательности:** **Диаграмма последовательности:**
@@ -63,7 +63,7 @@
### Удалить товар из корзины ### Удалить товар из корзины
Метод полностью удаляет все количество товара из корзины пользователя. Если у пользователя вовсе нет данной позиции, Метод полностью удаляет все количество товара из корзины пользователя. Если у пользователя вовсе нет данной позиции,
то возвращается такой же ответ, как будто бы все позиции данного sku были успешно удалены. то возвращается такой же ответ, как будто бы все позиции данного sku были успешно удалены.
| Метод | URI | | Метод | URI |
@@ -95,7 +95,7 @@
### Очистить корзину пользователя ### Очистить корзину пользователя
Метод полностью очищает корзину пользователя. Если у пользователя нет корзины или она пуста, то, как и при успешной Метод полностью очищает корзину пользователя. Если у пользователя нет корзины или она пуста, то, как и при успешной
очистке корзины, необходимо вернуть код ответа 204 No Content. очистке корзины, необходимо вернуть код ответа 204 No Content.
| Метод | URI | | Метод | URI |
@@ -125,7 +125,7 @@
### Получить содержимое корзины ### Получить содержимое корзины
Метод возвращает содержимое корзины пользователя на текущий момент. Если корзины у переданного пользователя нет, Метод возвращает содержимое корзины пользователя на текущий момент. Если корзины у переданного пользователя нет,
либо она пуста, следует вернуть 404 код ответа. Товары в корзине упорядочены в порядке возрастания sku. либо она пуста, следует вернуть 404 код ответа. Товары в корзине упорядочены в порядке возрастания sku.
| Метод | URI | | Метод | URI |
@@ -188,14 +188,14 @@
Если, вызвав `make run-all`, развернуть деплоймент, swagger этого сервиса Если, вызвав `make run-all`, развернуть деплоймент, swagger этого сервиса
можно увидеть локально по адресу: [http://localhost:8082/docs/](http://localhost:8082/docs/) можно увидеть локально по адресу: [http://localhost:8082/docs/](http://localhost:8082/docs/)
Сервис поддерживает следующие операции: Сервис поддерживает следующие операции:
#### GET /product?count=10&start_after_sku=0 #### GET /product?count=10&start_after_sku=0
Эта операция имеет два необязательных параметра: Эта операция имеет два необязательных параметра:
- `count` — сколько элементов вернуть и - `count` — сколько элементов вернуть и
- `start_after_sku` — после какого элемента начать вывод. - `start_after_sku` — после какого элемента начать вывод.
Response: Response:
``` ```
@@ -221,25 +221,25 @@ Response:
} }
``` ```
Обратите внимание, сервис `products` отдаёт цены в `int32` формате. Обратите внимание, сервис `product-service` отдаёт цены в `int32` формате.
В JSON нет беззнаковых целых, поэтому `int32`. В JSON нет беззнаковых целых, поэтому `int32`.
Вам же, внутри своих сервисов, для совместимости их между собой Вам же, внутри своих сервисов, для совместимости их между собой
следует использовать беззнаковый тип `uint32`. следует использовать беззнаковый тип `uint32`.
Также следует обратить внимание на то, что он выведен на `localhost:8082` лишь Также следует обратить внимание на то, что он выведен на `localhost:8082` лишь
для вашего удобства работы с данными. для вашего удобства работы с данными.
Ваш сервис `cart`, запущенный докер-контейнером, сможет подключиться к `products` Ваш сервис `cart`, запущенный докер-контейнером, сможет подключиться к `product-service`
по адресу `products:8082` — заниматься маршрутизацией будет сам докер. по адресу `product-service:8082` — заниматься маршрутизацией будет сам докер.
Авторизация на запрос выполняется с помощью заголовка `X-API-KEY` и токена `testToken`. Авторизация на запрос выполняется с помощью заголовка `X-API-KEY` и токена `testToken`.
## Makefile ## Makefile
В рамках данного задания необходимо имплементировать следующие таргеты: В рамках данного задания необходимо имплементировать следующие таргеты:
- run-all — запускает сервисы. На данный момент их должно стать два: - run-all — запускает сервисы. На данный момент их должно стать два:
- `products` — уже реализован - `product-service` — уже реализован
- `cart` — разрабатываемый вами в качестве домашнего задания - `cart` — разрабатываемый вами в качестве домашнего задания
## Ожидаемый результат ## Ожидаемый результат
@@ -270,5 +270,5 @@ Response:
Сценарий тестирования следует описать в [cart.http](./cart.http) Сценарий тестирования следует описать в [cart.http](./cart.http)
### Дедлайны сдачи и проверки задания: ### Дедлайны сдачи и проверки задания:
- 24 мая 23:59 (сдача) / 27 мая, 23:59 (проверка) - 24 мая 23:59 (сдача) / 27 мая, 23:59 (проверка)

View File

@@ -66,7 +66,7 @@ Content-Type: application/json
### delete whole sku from cart ### delete whole sku from cart
DELETE http://localhost:8080/user/31337/cart/1076963 DELETE http://localhost:8080/user/31337/cart/1076963
Content-Type: application/json Content-Type: application/json
### expected {} 200 OK; must delete item from cart ### expected {} 204 No Content; must delete item from cart
### delete whole cart ### delete whole cart
DELETE http://localhost:8080/user/31337/cart DELETE http://localhost:8080/user/31337/cart

33
docs/homework-2/README.md Normal file
View File

@@ -0,0 +1,33 @@
# Домашнее задание по модулю "Тестирование в Go"
Необходимо покрыть Unit-тестами слой `UseCase` для всех запросов сервиса Cart. Покрыть Unit-тестами репозиторий сервиса Cart (in-memory)
## Основное задание
1. Написаны Unit-тесты слоя `UseCase` для каждого запроса
2. Написаны Unit-тесты слоя `Repository` сервиса Cart
3. Процент покрытия тестируемых слоев должен быть не менее 60%
4. Использовать библиотеку `minimock` для создания моков (библиотека minimock — https://github.com/gojuno/minimock)
5. В `Makefile` создать команду для расчета coverage
## Дополнительное задание
1. Написать два позитивных e2e теста: для хендлеров `DELETE /user/<user_id>/cart/<sku_id>` и `GET /user/<user_id>/cart`
2. Настроить линтер для проверки цикломатической и когнитивной сложности (https://github.com/fzipp/gocyclo и https://github.com/uudashr/gocognit)
3. Написать бенчмарк для `In-memory Storage` сервиса Cart (хотя бы одну операцию — добавление/удаление и т.д.)
## Автоматические проверки
Ваше решение должно проходить автоматические проверки:
- Компиляция
- Линтер
- Unit-тесты
- Code coverage >40%
- Автотесты
Прохождение автоматических проверок влияет на итоговую оценку за домашнюю работу.
### Дедлайны сдачи и проверки задания:
- 31 мая 23:59 (сдача) / 3 июня, 23:59 (проверка)

View File

@@ -1,4 +1,4 @@
go 1.23.1 go 1.23.9
use ( use (
./cart ./cart

View File

@@ -1,8 +1,16 @@
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/hexdigest/gowrap v1.4.2/go.mod h1:s+1hE6qakgdaaLqgdwPAj5qKYVBCSbPJhEbx+I1ef/Q=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=

28
make/coverage.mk Normal file
View File

@@ -0,0 +1,28 @@
COV_REPORT_DIR := .coverage
define coverage
@if [ -f "$(1)/go.mod" ]; then \
echo "===== coverage for $(1) ====="; \
outdir=$(COV_REPORT_DIR)/$(1); \
mkdir -p $$outdir; \
pkgs=$$(go list ./$(1)/... | grep -vE 'mock|config|generated|header|document|internal/pb'); \
if [ -z "$$pkgs" ]; then \
echo "no packages to test in $(1)"; exit 0; \
fi; \
go test -race -covermode=atomic -coverprofile=$$outdir/coverage.out $$pkgs; \
go tool cover -html=$$outdir/coverage.out -o $$outdir/coverage.html; \
echo "html report: $$outdir/coverage.html"; \
fi
endef
cart-coverage:
$(call coverage,cart)
loms-coverage:
$(call coverage,loms)
notifier-coverage:
$(call coverage,notifier)
comments-coverage:
$(call coverage,comments)

25
make/generate.mk Normal file
View File

@@ -0,0 +1,25 @@
define generate
@if [ -f "$(1)/go.mod" ]; then \
echo "===== running go generate for $(1) ====="; \
if [ ! -d "$(1)/internal" ]; then \
echo "no internal package in $(1)"; \
exit 0; \
fi; \
go generate ./$(1)/internal/...; \
fi
endef
cart-generate:
$(call generate,cart)
loms-generate:
$(call generate,loms)
notifier-generate:
$(call generate,notifier)
comments-generate:
$(call generate,comments)