From 4e86adffb79de87b9f8cbbb391a87cc0ae42f7d7 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Wed, 22 Apr 2026 13:18:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E6=80=A7=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=E5=90=8E=E7=AB=AF=E6=B5=8B=E8=AF=95=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 6 个测试场景 (config load pipe, handler errors, service aggregation, engine degradation, openai decoder edges, negative tests) - 更新测试工具规格 (mockgen, in-memory SQLite) - 覆盖率目标从 >80% 提升至 >85% - 新增 test-unit 和 test-integration Makefile 命令 - 新增死代码清理和 mockgen 需求 - 归档变更至 openspec/changes/archive/2026-04-22-improve-backend-testing/ --- backend/Makefile | 16 +- backend/go.mod | 165 ++++ backend/go.sum | 890 ++++++++++++++++- .../conversion/engine_supplemental_test.go | 55 ++ backend/internal/conversion/engine_test.go | 32 +- .../conversion/openai/decoder_test.go | 22 + .../handler/handler_supplemental_test.go | 75 +- backend/internal/handler/handler_test.go | 387 +++++--- .../internal/handler/proxy_handler_test.go | 933 +++++++++--------- backend/internal/provider/client.go | 1 + backend/internal/repository/model_repo.go | 2 + backend/internal/repository/provider_repo.go | 5 +- .../internal/repository/provider_repo_impl.go | 19 - .../internal/repository/repository_test.go | 77 +- backend/internal/repository/stats_repo.go | 2 + backend/internal/service/model_service.go | 2 + backend/internal/service/provider_service.go | 2 + backend/internal/service/routing_service.go | 2 + .../service/service_supplemental_test.go | 10 +- backend/internal/service/service_test.go | 267 ++++- backend/internal/service/stats_service.go | 2 + backend/tests/config/config_test.go | 193 ++++ backend/tests/helpers.go | 25 +- backend/tests/mocks/mock_model_repository.go | 143 +++ backend/tests/mocks/mock_model_service.go | 128 +++ backend/tests/mocks/mock_provider_client.go | 73 ++ .../tests/mocks/mock_provider_repository.go | 113 +++ backend/tests/mocks/mock_provider_service.go | 143 +++ backend/tests/mocks/mock_routing_service.go | 56 ++ backend/tests/mocks/mock_stats_repository.go | 71 ++ backend/tests/mocks/mock_stats_service.go | 85 ++ openspec/specs/test-coverage/spec.md | 107 +- 32 files changed, 3374 insertions(+), 729 deletions(-) create mode 100644 backend/tests/config/config_test.go create mode 100644 backend/tests/mocks/mock_model_repository.go create mode 100644 backend/tests/mocks/mock_model_service.go create mode 100644 backend/tests/mocks/mock_provider_client.go create mode 100644 backend/tests/mocks/mock_provider_repository.go create mode 100644 backend/tests/mocks/mock_provider_service.go create mode 100644 backend/tests/mocks/mock_routing_service.go create mode 100644 backend/tests/mocks/mock_stats_repository.go create mode 100644 backend/tests/mocks/mock_stats_service.go diff --git a/backend/Makefile b/backend/Makefile index 8eefd5d..0003a85 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -1,4 +1,4 @@ -.PHONY: build run test test-coverage clean migrate-up migrate-down migrate-status migrate-create lint +.PHONY: build run test test-unit test-integration test-coverage clean migrate-up migrate-down migrate-status migrate-create lint generate deps # 构建 build: @@ -12,6 +12,14 @@ run: test: go test ./... -v +# 单元测试 +test-unit: + go test ./internal/... ./pkg/... -v + +# 集成测试 +test-integration: + go test ./tests/... -v + # 测试覆盖率 test-coverage: go test ./... -coverprofile=coverage.out @@ -38,8 +46,12 @@ migrate-create: # 代码检查 lint: - golangci-lint run ./... + go tool golangci-lint run ./... # 安装依赖 deps: go mod tidy + +# 生成代码(mock 等) +generate: + go generate ./... diff --git a/backend/go.mod b/backend/go.mod index 71a791a..8a59480 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,6 +2,11 @@ module nex/backend go 1.26.2 +tool ( + github.com/golangci/golangci-lint/cmd/golangci-lint + go.uber.org/mock/mockgen +) + require ( github.com/gin-gonic/gin v1.12.0 github.com/go-playground/validator/v10 v10.30.2 @@ -11,6 +16,7 @@ require ( github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + go.uber.org/mock v0.6.0 go.uber.org/zap v1.27.1 gopkg.in/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 @@ -19,52 +25,211 @@ require ( ) require ( + 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect + 4d63.com/gochecknoglobals v0.2.2 // indirect + github.com/4meepo/tagalign v1.4.2 // indirect + github.com/Abirdcfly/dupword v0.1.3 // indirect + github.com/Antonboom/errname v1.0.0 // indirect + github.com/Antonboom/nilnil v1.0.1 // indirect + github.com/Antonboom/testifylint v1.5.2 // indirect github.com/BurntSushi/toml v1.6.0 // indirect + github.com/Crocmagnon/fatcontext v0.7.1 // indirect + github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect + github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect + github.com/alecthomas/go-check-sumtype v0.3.1 // indirect + github.com/alexkohler/nakedret/v2 v2.0.5 // indirect + github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alingse/asasalint v0.0.11 // indirect + github.com/alingse/nilnesserr v0.1.2 // indirect + github.com/ashanbrown/forbidigo v1.6.0 // indirect + github.com/ashanbrown/makezero v1.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bkielbasa/cyclop v1.2.3 // indirect + github.com/blizzy78/varnamelen v0.8.0 // indirect + github.com/bombsimon/wsl/v4 v4.5.0 // indirect + github.com/breml/bidichk v0.3.2 // indirect + github.com/breml/errchkjson v0.4.0 // indirect + github.com/butuzov/ireturn v0.3.1 // indirect + github.com/butuzov/mirror v1.3.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/catenacyber/perfsprint v0.8.2 // indirect + github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charithe/durationcheck v0.0.10 // indirect + github.com/chavacava/garif v0.1.0 // indirect + github.com/ckaznocha/intrange v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect + github.com/curioswitch/go-reassign v0.3.0 // indirect + github.com/daixiang0/gci v0.13.5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/denis-tingaikin/go-header v0.5.0 // indirect + github.com/ettle/strcase v0.2.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/firefart/nonamedreturns v1.0.5 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fzipp/gocyclo v0.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/ghostiam/protogetter v0.3.9 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-critic/go-critic v0.12.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-toolsmith/astcast v1.1.0 // indirect + github.com/go-toolsmith/astcopy v1.1.0 // indirect + github.com/go-toolsmith/astequal v1.2.0 // indirect + github.com/go-toolsmith/astfmt v1.1.0 // indirect + github.com/go-toolsmith/astp v1.1.0 // indirect + github.com/go-toolsmith/strparse v1.1.0 // indirect + github.com/go-toolsmith/typep v1.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect + github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/gofrs/flock v0.12.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect + github.com/golangci/go-printf-func-name v0.1.0 // indirect + github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect + github.com/golangci/golangci-lint v1.64.8 // indirect + github.com/golangci/misspell v0.6.0 // indirect + github.com/golangci/plugin-module-register v0.1.1 // indirect + github.com/golangci/revgrep v0.8.0 // indirect + github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/gordonklaus/ineffassign v0.1.0 // indirect + github.com/gostaticanalysis/analysisutil v0.7.1 // indirect + github.com/gostaticanalysis/comment v1.5.0 // indirect + github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect + github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jgautheron/goconst v1.7.1 // indirect + github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jjti/go-spancheck v0.6.4 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/julz/importas v0.2.0 // indirect + github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect + github.com/kisielk/errcheck v1.9.0 // indirect + github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kulti/thelper v0.6.3 // indirect + github.com/kunwardeep/paralleltest v1.0.10 // indirect + github.com/lasiar/canonicalheader v1.1.2 // indirect + github.com/ldez/exptostd v0.4.2 // indirect + github.com/ldez/gomoddirectives v0.6.1 // indirect + github.com/ldez/grignotin v0.9.0 // indirect + github.com/ldez/tagliatelle v0.7.1 // indirect + github.com/ldez/usetesting v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/leonklingele/grouper v1.1.2 // indirect + github.com/macabu/inamedparam v0.1.3 // indirect + github.com/maratori/testableexamples v1.0.0 // indirect + github.com/maratori/testpackage v1.1.1 // indirect + github.com/matoous/godox v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mfridman/interpolate v0.0.2 // indirect + github.com/mgechev/revive v1.7.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/moricho/tparallel v0.3.2 // indirect + github.com/nakabonne/nestif v0.3.1 // indirect + github.com/nishanths/exhaustive v0.12.0 // indirect + github.com/nishanths/predeclared v0.2.2 // indirect + github.com/nunnatsa/ginkgolinter v0.19.1 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/polyfloyd/go-errorlint v1.7.1 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 // indirect + github.com/quasilyte/go-ruleguard/dsl v0.3.22 // indirect + github.com/quasilyte/gogrep v0.5.0 // indirect + github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect + github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect + github.com/raeperd/recvcheck v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/ryancurrah/gomodguard v1.3.5 // indirect + github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/sashamelentyev/interfacebloat v1.1.0 // indirect + github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect + github.com/securego/gosec/v2 v2.22.2 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sivchari/containedctx v1.0.3 // indirect + github.com/sivchari/tenv v1.12.1 // indirect + github.com/sonatard/noctx v0.1.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect + github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tdakkota/asciicheck v0.4.1 // indirect + github.com/tetafro/godot v1.5.0 // indirect + github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect + github.com/timonwong/loggercheck v0.10.1 // indirect + github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect + github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect + github.com/ultraware/funlen v0.2.0 // indirect + github.com/ultraware/whitespace v0.2.0 // indirect + github.com/uudashr/gocognit v1.2.0 // indirect + github.com/uudashr/iface v1.3.1 // indirect + github.com/xen0n/gosmopolitan v1.2.2 // indirect + github.com/yagipy/maintidx v1.0.0 // indirect + github.com/yeya24/promlinter v0.3.0 // indirect + github.com/ykadowak/zerologlint v0.1.5 // indirect + gitlab.com/bosi/decorder v0.4.2 // indirect + go-simpler.org/musttag v0.13.0 // indirect + go-simpler.org/sloglint v0.9.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.22.0 // indirect golang.org/x/crypto v0.49.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect + golang.org/x/tools v0.42.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect + golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + honnef.co/go/tools v0.6.1 // indirect + mvdan.cc/gofumpt v0.7.0 // indirect + mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f // indirect ) diff --git a/backend/go.sum b/backend/go.sum index 03f34e9..4018f0a 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,29 +1,187 @@ +4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= +4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= +4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= +4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= +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.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= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +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= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +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/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= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +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= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= +github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= +github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= +github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +github.com/Antonboom/errname v1.0.0 h1:oJOOWR07vS1kRusl6YRSlat7HFnb3mSfMl6sDMRoTBA= +github.com/Antonboom/errname v1.0.0/go.mod h1:gMOBFzK/vrTiXN9Oh+HFs+e6Ndl0eTFbtsRTSRdXyGI= +github.com/Antonboom/nilnil v1.0.1 h1:C3Tkm0KUxgfO4Duk3PM+ztPncTFlOf0b2qadmS0s4xs= +github.com/Antonboom/nilnil v1.0.1/go.mod h1:CH7pW2JsRNFgEh8B2UaPZTEPhCMuFowP/e8Udp9Nnb0= +github.com/Antonboom/testifylint v1.5.2 h1:4s3Xhuv5AvdIgbd8wOOEeo0uZG7PbDKQyKY5lGoQazk= +github.com/Antonboom/testifylint v1.5.2/go.mod h1:vxy8VJ0bc6NavlYqjZfmp6EfqXMtBgQ4+mhCojwC1P8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Crocmagnon/fatcontext v0.7.1 h1:SC/VIbRRZQeQWj/TcQBS6JmrXcfA+BU4OGSVUt54PjM= +github.com/Crocmagnon/fatcontext v0.7.1/go.mod h1:1wMvv3NXEBJucFGfwOJBxSVWcoIO6emV215SMkW9MFU= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= +github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= +github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= +github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= +github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= +github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= +github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexkohler/nakedret/v2 v2.0.5 h1:fP5qLgtwbx9EJE8dGEERT02YwS8En4r9nnZ71RK+EVU= +github.com/alexkohler/nakedret/v2 v2.0.5/go.mod h1:bF5i0zF2Wo2o4X4USt9ntUWve6JbFv02Ff4vlkmS/VU= +github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= +github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= +github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= +github.com/alingse/nilnesserr v0.1.2 h1:Yf8Iwm3z2hUUrP4muWfW83DF4nE3r1xZ26fGWUKCZlo= +github.com/alingse/nilnesserr v0.1.2/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= +github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= +github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= +github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= +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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= +github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= +github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= +github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= +github.com/bombsimon/wsl/v4 v4.5.0 h1:iZRsEvDdyhd2La0FVi5k6tYehpOR/R7qIUjmKk7N74A= +github.com/bombsimon/wsl/v4 v4.5.0/go.mod h1:NOQ3aLF4nD7N5YPXMruR6ZXDOAqLoM0GEpLwTdvmOSc= +github.com/breml/bidichk v0.3.2 h1:xV4flJ9V5xWTqxL+/PMFF6dtJPvZLPsyixAoPe8BGJs= +github.com/breml/bidichk v0.3.2/go.mod h1:VzFLBxuYtT23z5+iVkamXO386OB+/sVwZOpIj6zXGos= +github.com/breml/errchkjson v0.4.0 h1:gftf6uWZMtIa/Is3XJgibewBm2ksAQSY/kABDNFTAdk= +github.com/breml/errchkjson v0.4.0/go.mod h1:AuBOSTHyLSaaAFlWsRSuRBIroCh3eh7ZHh5YeelDIk8= +github.com/butuzov/ireturn v0.3.1 h1:mFgbEI6m+9W8oP/oDdfA34dLisRFCj2G6o/yiI1yZrY= +github.com/butuzov/ireturn v0.3.1/go.mod h1:ZfRp+E7eJLC0NQmk1Nrm1LOrn/gQlOykv+cVPdiXH5M= +github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= +github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/catenacyber/perfsprint v0.8.2 h1:+o9zVmCSVa7M4MvabsWvESEhpsMkhfE7k0sHNGL95yw= +github.com/catenacyber/perfsprint v0.8.2/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= +github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= +github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charithe/durationcheck v0.0.10 h1:wgw73BiocdBDQPik+zcEoBG/ob8uyBHf2iyoHGPf5w4= +github.com/charithe/durationcheck v0.0.10/go.mod h1:bCWXb7gYRysD1CU3C+u4ceO49LoGOY1C1L6uouGNreQ= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= +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/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= +github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= +github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= +github.com/daixiang0/gci v0.13.5 h1:kThgmH1yBmZSBCh1EJVxQ7JsHpm5Oms0AMed/0LaH4c= +github.com/daixiang0/gci v0.13.5/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= +github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/firefart/nonamedreturns v1.0.5 h1:tM+Me2ZaXs8tfdDw3X6DOX++wMCOqzYUho6tUTYIdRA= +github.com/firefart/nonamedreturns v1.0.5/go.mod h1:gHJjDqhGM4WyPt639SOZs+G89Ko7QKH5R5BhnO6xJhw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= +github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/ghostiam/protogetter v0.3.9 h1:j+zlLLWzqLay22Cz/aYwTHKQ88GE2DQ6GkWSYFOI4lQ= +github.com/ghostiam/protogetter v0.3.9/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-critic/go-critic v0.12.0 h1:iLosHZuye812wnkEz1Xu3aBwn5ocCPfc9yqmFG9pa6w= +github.com/go-critic/go-critic v0.12.0/go.mod h1:DpE0P6OVc6JzVYzmM5gq5jMU31zLr4am5mB/VfFK64w= +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-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +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-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-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= @@ -32,80 +190,400 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ= github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= +github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= +github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= +github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= +github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= +github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= +github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= +github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= +github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= +github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= +github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= +github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= +github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= +github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= +github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= +github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= +github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= +github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= +github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= +github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +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/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= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= +github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUPPyAKJuzv8pEJU= +github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= +github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I= +github.com/golangci/golangci-lint v1.64.8/go.mod h1:5cEsUQBSr6zi8XI8OjmcY2Xmliqc4iYL7YoPrL+zLJ4= +github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= +github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= +github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= +github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= +github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs= +github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed/go.mod h1:XLXN8bNw4CGRPaqgl3bv/lhz7bsGPh4/xSaMTbo2vkQ= +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/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= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/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/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= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/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/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= +github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= +github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= +github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= +github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado= +github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= +github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= +github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= +github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= +github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= +github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3Uqrmrcpk= +github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= +github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= +github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= +github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= +github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jgautheron/goconst v1.7.1 h1:VpdAG7Ca7yvvJk5n8dMwQhfEZJh95kl/Hl9S1OI5Jkk= +github.com/jgautheron/goconst v1.7.1/go.mod h1:aAosetZ5zaeC/2EfMeRswtxUFBpe2Hr7HzkgX4fanO4= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= +github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +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/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= +github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= +github.com/karamaru-alpha/copyloopvar v1.2.1 h1:wmZaZYIjnJ0b5UoKDjUHrikcV0zuPyyxI4SVplLd2CI= +github.com/karamaru-alpha/copyloopvar v1.2.1/go.mod h1:nFmMlFNlClC2BPvNaHMdkirmTJxVCY0lhxBtlfOypMM= +github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= +github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= +github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +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.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= +github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= +github.com/kunwardeep/paralleltest v1.0.10 h1:wrodoaKYzS2mdNVnc4/w31YaXFtsc21PCTdvWJ/lDDs= +github.com/kunwardeep/paralleltest v1.0.10/go.mod h1:2C7s65hONVqY7Q5Efj5aLzRCNLjw2h4eMc9EcypGjcY= +github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= +github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= +github.com/ldez/exptostd v0.4.2 h1:l5pOzHBz8mFOlbcifTxzfyYbgEmoUqjxLFHZkjlbHXs= +github.com/ldez/exptostd v0.4.2/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= +github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= +github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= +github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= +github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= +github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= +github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= +github.com/ldez/usetesting v0.4.2 h1:J2WwbrFGk3wx4cZwSMiCQQ00kjGR0+tuuyW0Lqm4lwA= +github.com/ldez/usetesting v0.4.2/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= 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/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= +github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= +github.com/macabu/inamedparam v0.1.3 h1:2tk/phHkMlEL/1GNe/Yf6kkR/hkcUdAEY3L0hjYV1Mk= +github.com/macabu/inamedparam v0.1.3/go.mod h1:93FLICAIk/quk7eaPPQvbzihUdn/QkGDwIZEoLtpH6I= +github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= +github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= +github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= +github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc= +github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= +github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY= +github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= +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/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= +github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= +github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= +github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4= +github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyfloyd/go-errorlint v1.7.1 h1:RyLVXIbosq1gBdk/pChWA8zWYLsq9UEw7a1L5TVMCnA= +github.com/polyfloyd/go-errorlint v1.7.1/go.mod h1:aXjNb1x2TNhoLsk26iv1yl7a+zTnXPhwEMtEXukiLR8= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/pressly/goose/v3 v3.27.0 h1:/D30gVTuQhu0WsNZYbJi4DMOsx1lNq+6SkLe+Wp59BM= github.com/pressly/goose/v3 v3.27.0/go.mod h1:3ZBeCXqzkgIRvrEMDkYh1guvtoJTU5oMMuDdkutoM78= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +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/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1 h1:+Wl/0aFp0hpuHM3H//KMft64WQ1yX9LdJY64Qm/gFCo= +github.com/quasilyte/go-ruleguard v0.4.3-0.20240823090925-0fe6f58b47b1/go.mod h1:GJLgqsLeo4qgavUoL8JeGFNS7qcisx3awV/w9eWTmNI= +github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE= +github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= +github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= +github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= +github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= +github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryancurrah/gomodguard v1.3.5 h1:cShyguSwUEeC0jS7ylOiG/idnd1TpJ1LfHGpV3oJmPU= +github.com/ryancurrah/gomodguard v1.3.5/go.mod h1:MXlEPQRxgfPQa62O8wzK3Ozbkv9Rkqr+wKjSxTdsNJE= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= +github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= +github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= +github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= +github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= +github.com/securego/gosec/v2 v2.22.2 h1:IXbuI7cJninj0nRpZSLCUlotsj8jGusohfONMrHoF6g= +github.com/securego/gosec/v2 v2.22.2/go.mod h1:UEBGA+dSKb+VqM6TdehR7lnQtIIMorYJ4/9CW1KVQBE= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= +github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= +github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= +github.com/sivchari/tenv v1.12.1/go.mod h1:1LjSOUCc25snIr5n3DtGGrENhX3LuWefcplwVGC24mw= +github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= +github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= +github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= +github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= +github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= 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/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.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -114,12 +592,66 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8= +github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8= +github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA= +github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpRQGxTSkNYKJ51yaw6ChIqO+Je8UqsTKN/cDag= +github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= +github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw= +github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio= +github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg= +github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= +github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg= +github.com/timonwong/loggercheck v0.10.1/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= +github.com/tomarrell/wrapcheck/v2 v2.10.0 h1:SzRCryzy4IrAH7bVGG4cK40tNUhmVmMDuJujy4XwYDg= +github.com/tomarrell/wrapcheck/v2 v2.10.0/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo= +github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= +github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= +github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= +github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= +github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= +github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= +github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= +github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= +github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= +github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= +github.com/xen0n/gosmopolitan v1.2.2/go.mod h1:7XX7Mj61uLYrj0qmeN0zi7XDon9JRAEhYQqAPLVNTeg= +github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= +github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= +github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= +github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= +github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= +github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= +gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= +go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= +go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= +go-simpler.org/musttag v0.13.0 h1:Q/YAW0AHvaoaIbsPj3bvEI5/QFP7w696IMUpnKXQfCE= +go-simpler.org/musttag v0.13.0/go.mod h1:FTzIGeK6OkKlUDVpj0iQUXZLUO1Js9+mvykDQy9C5yM= +go-simpler.org/sloglint v0.9.0 h1:/40NQtjRx9txvsB/RN022KsUJU+zaaSb/9q9BSefSrE= +go-simpler.org/sloglint v0.9.0/go.mod h1:G/OrAF6uxj48sHahCzrbarVMptL2kjWTaUeC8+fOGww= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +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= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= @@ -132,28 +664,366 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +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= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +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-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= +golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +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= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +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/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= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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/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.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +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-20181114220301-adae6a3d119a/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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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-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= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/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-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +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-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= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +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= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/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= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20181116152217-5ac8a444bdc5/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-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-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-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/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= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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-20200615200032-f1bc736245b1/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-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-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210603081109-ebe580a85c40/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-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +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/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-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-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-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-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-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= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200324003944-a576cf524670/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200820010801-b793a1359eac/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20201023174141-c8cfbd0f21e6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +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/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +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= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +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-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= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/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.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= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +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/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= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +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.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/lumberjack.v2 v2.0.0 h1:IDj6hi8KbNiPQ5VaYNFZ7dBJLF5LFeKvsFrWHjA5aq4= gopkg.in/lumberjack.v2 v2.0.0/go.mod h1:bp5nQ2kK/lLQSmTk29azj9+JB6bWci56xFn/lvd5GLI= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/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= @@ -163,6 +1033,15 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +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= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= +honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ= modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -171,3 +1050,10 @@ modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f h1:lMpcwN6GxNbWtbpI1+xzFLSW8XzX0u72NttUGVFjO3U= +mvdan.cc/unparam v0.0.0-20240528143540-8a5130ca722f/go.mod h1:RSLa7mKKCNeTTMHBw5Hsy2rfJmd6O2ivt9Dw9ZqCQpQ= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/backend/internal/conversion/engine_supplemental_test.go b/backend/internal/conversion/engine_supplemental_test.go index eec0c16..1a8cc07 100644 --- a/backend/internal/conversion/engine_supplemental_test.go +++ b/backend/internal/conversion/engine_supplemental_test.go @@ -321,3 +321,58 @@ func (m *testMiddleware) InterceptStreamEvent(event *canonical.CanonicalStreamEv } var _ = json.Marshal + +func TestConvertEmbeddingBody_DecodeError(t *testing.T) { + registry := NewMemoryRegistry() + engine := NewConversionEngine(registry, nil) + + clientAdapter := newMockAdapter("client", false) + clientAdapter.decodeEmbeddingReqFn = func(raw []byte) (*canonical.CanonicalEmbeddingRequest, error) { + return nil, errors.New("decode embedding failed") + } + clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true} + providerAdapter := newMockAdapter("provider", false) + providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true} + _ = engine.RegisterAdapter(clientAdapter) + _ = engine.RegisterAdapter(providerAdapter) + + body := []byte(`{"model":"text-embedding","input":"hello"}`) + result, err := engine.convertEmbeddingBody(clientAdapter, providerAdapter, NewTargetProvider("", "", ""), body) + require.NoError(t, err) + assert.Equal(t, body, result) +} + +func TestConvertRerankBody_DecodeError(t *testing.T) { + registry := NewMemoryRegistry() + engine := NewConversionEngine(registry, nil) + + clientAdapter := newMockAdapter("client", false) + clientAdapter.decodeRerankReqFn = func(raw []byte) (*canonical.CanonicalRerankRequest, error) { + return nil, errors.New("decode rerank failed") + } + clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true} + providerAdapter := newMockAdapter("provider", false) + providerAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true} + _ = engine.RegisterAdapter(clientAdapter) + _ = engine.RegisterAdapter(providerAdapter) + + body := []byte(`{"model":"rerank","query":"test","documents":["a"]}`) + result, err := engine.convertRerankBody(clientAdapter, providerAdapter, NewTargetProvider("", "", ""), body) + require.NoError(t, err) + assert.Equal(t, body, result) +} + +func TestConvertBody_UnknownInterfaceType(t *testing.T) { + registry := NewMemoryRegistry() + engine := NewConversionEngine(registry, nil) + + clientAdapter := newMockAdapter("client", false) + providerAdapter := newMockAdapter("provider", false) + _ = engine.RegisterAdapter(clientAdapter) + _ = engine.RegisterAdapter(providerAdapter) + + body := []byte(`{"test":"data"}`) + result, err := engine.convertBody(InterfaceType("UNKNOWN"), clientAdapter, providerAdapter, NewTargetProvider("", "", ""), body) + require.NoError(t, err) + assert.Equal(t, body, result) +} diff --git a/backend/internal/conversion/engine_test.go b/backend/internal/conversion/engine_test.go index eede79a..26bbb10 100644 --- a/backend/internal/conversion/engine_test.go +++ b/backend/internal/conversion/engine_test.go @@ -13,18 +13,20 @@ import ( // mockProtocolAdapter 模拟协议适配器 type mockProtocolAdapter struct { - protocolName string - passthrough bool - ifaceType InterfaceType - supportsIface map[InterfaceType]bool - decodeReqFn func([]byte) (*canonical.CanonicalRequest, error) - encodeReqFn func(*canonical.CanonicalRequest, *TargetProvider) ([]byte, error) - decodeRespFn func([]byte) (*canonical.CanonicalResponse, error) - encodeRespFn func(*canonical.CanonicalResponse) ([]byte, error) - streamDecoderFn func() StreamDecoder - streamEncoderFn func() StreamEncoder - rewriteReqFn func([]byte, string, InterfaceType) ([]byte, error) - rewriteRespFn func([]byte, string, InterfaceType) ([]byte, error) + protocolName string + passthrough bool + ifaceType InterfaceType + supportsIface map[InterfaceType]bool + decodeReqFn func([]byte) (*canonical.CanonicalRequest, error) + encodeReqFn func(*canonical.CanonicalRequest, *TargetProvider) ([]byte, error) + decodeRespFn func([]byte) (*canonical.CanonicalResponse, error) + encodeRespFn func(*canonical.CanonicalResponse) ([]byte, error) + streamDecoderFn func() StreamDecoder + streamEncoderFn func() StreamEncoder + rewriteReqFn func([]byte, string, InterfaceType) ([]byte, error) + rewriteRespFn func([]byte, string, InterfaceType) ([]byte, error) + decodeEmbeddingReqFn func([]byte) (*canonical.CanonicalEmbeddingRequest, error) + decodeRerankReqFn func([]byte) (*canonical.CanonicalRerankRequest, error) } func newMockAdapter(name string, passthrough bool) *mockProtocolAdapter { @@ -126,6 +128,9 @@ func (m *mockProtocolAdapter) EncodeModelInfoResponse(info *canonical.CanonicalM } func (m *mockProtocolAdapter) DecodeEmbeddingRequest(raw []byte) (*canonical.CanonicalEmbeddingRequest, error) { + if m.decodeEmbeddingReqFn != nil { + return m.decodeEmbeddingReqFn(raw) + } return &canonical.CanonicalEmbeddingRequest{}, nil } @@ -142,6 +147,9 @@ func (m *mockProtocolAdapter) EncodeEmbeddingResponse(resp *canonical.CanonicalE } func (m *mockProtocolAdapter) DecodeRerankRequest(raw []byte) (*canonical.CanonicalRerankRequest, error) { + if m.decodeRerankReqFn != nil { + return m.decodeRerankReqFn(raw) + } return &canonical.CanonicalRerankRequest{}, nil } diff --git a/backend/internal/conversion/openai/decoder_test.go b/backend/internal/conversion/openai/decoder_test.go index 3babfc0..c83109c 100644 --- a/backend/internal/conversion/openai/decoder_test.go +++ b/backend/internal/conversion/openai/decoder_test.go @@ -409,3 +409,25 @@ func TestDecodeResponse_Refusal(t *testing.T) { } assert.True(t, found) } + +func TestDecodeRequest_AssistantContentArray(t *testing.T) { + body := []byte(`{ + "model": "gpt-4", + "messages": [ + {"role": "user", "content": "hello"}, + { + "role": "assistant", + "content": [{"type": "text", "text": "hello back"}] + } + ] + }`) + + req, err := decodeRequest(body) + require.NoError(t, err) + assert.Len(t, req.Messages, 2) + assistantMsg := req.Messages[1] + assert.Equal(t, canonical.RoleAssistant, assistantMsg.Role) + assert.Len(t, assistantMsg.Content, 1) + assert.Equal(t, "text", assistantMsg.Content[0].Type) + assert.Equal(t, "hello back", assistantMsg.Content[0].Text) +} diff --git a/backend/internal/handler/handler_supplemental_test.go b/backend/internal/handler/handler_supplemental_test.go index 716c406..a99d886 100644 --- a/backend/internal/handler/handler_supplemental_test.go +++ b/backend/internal/handler/handler_supplemental_test.go @@ -9,12 +9,19 @@ import ( "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "nex/backend/internal/domain" + "nex/backend/tests/mocks" ) func TestProviderHandler_CreateProvider_Success(t *testing.T) { - h := NewProviderHandler(&mockProviderService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).Return(nil) + h := NewProviderHandler(mockSvc) body, _ := json.Marshal(map[string]string{ "id": "p1", @@ -37,7 +44,12 @@ func TestProviderHandler_CreateProvider_Success(t *testing.T) { } func TestProviderHandler_CreateProvider_WithProtocol(t *testing.T) { - h := NewProviderHandler(&mockProviderService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).Return(nil) + h := NewProviderHandler(mockSvc) body, _ := json.Marshal(map[string]string{ "id": "p1", @@ -56,9 +68,13 @@ func TestProviderHandler_CreateProvider_WithProtocol(t *testing.T) { } func TestProviderHandler_UpdateProvider(t *testing.T) { - h := NewProviderHandler(&mockProviderService{ - provider: &domain.Provider{ID: "p1", Name: "Updated", APIKey: "***"}, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Update(gomock.Eq("p1"), gomock.Any()).Return(nil) + mockSvc.EXPECT().Get(gomock.Eq("p1"), gomock.Eq(true)).Return(&domain.Provider{ID: "p1", Name: "Updated", APIKey: "***"}, nil) + h := NewProviderHandler(mockSvc) body, _ := json.Marshal(map[string]string{"name": "Updated"}) w := httptest.NewRecorder() @@ -72,7 +88,11 @@ func TestProviderHandler_UpdateProvider(t *testing.T) { } func TestProviderHandler_UpdateProvider_InvalidBody(t *testing.T) { - h := NewProviderHandler(&mockProviderService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + h := NewProviderHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -84,7 +104,12 @@ func TestProviderHandler_UpdateProvider_InvalidBody(t *testing.T) { } func TestProviderHandler_DeleteProvider(t *testing.T) { - h := NewProviderHandler(&mockProviderService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Delete(gomock.Eq("p1")).Return(nil) + h := NewProviderHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -97,7 +122,12 @@ func TestProviderHandler_DeleteProvider(t *testing.T) { } func TestModelHandler_DeleteModel(t *testing.T) { - h := NewModelHandler(&mockModelService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Delete(gomock.Eq("m1")).Return(nil) + h := NewModelHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -110,7 +140,15 @@ func TestModelHandler_DeleteModel(t *testing.T) { } func TestModelHandler_CreateModel_Success(t *testing.T) { - h := NewModelHandler(&mockModelService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).DoAndReturn(func(model *domain.Model) error { + model.ID = "mock-uuid-1234" + return nil + }) + h := NewModelHandler(mockSvc) body, _ := json.Marshal(map[string]string{ "provider_id": "p1", @@ -130,9 +168,12 @@ func TestModelHandler_CreateModel_Success(t *testing.T) { } func TestModelHandler_GetModel(t *testing.T) { - h := NewModelHandler(&mockModelService{ - model: &domain.Model{ID: "m1", ModelName: "gpt-4"}, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Get(gomock.Eq("m1")).Return(&domain.Model{ID: "m1", ModelName: "gpt-4"}, nil) + h := NewModelHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -148,9 +189,13 @@ func TestModelHandler_GetModel(t *testing.T) { } func TestModelHandler_UpdateModel(t *testing.T) { - h := NewModelHandler(&mockModelService{ - model: &domain.Model{ID: "m1", ModelName: "gpt-4o"}, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Update(gomock.Eq("m1"), gomock.Any()).Return(nil) + mockSvc.EXPECT().Get(gomock.Eq("m1")).Return(&domain.Model{ID: "m1", ModelName: "gpt-4o"}, nil) + h := NewModelHandler(mockSvc) body, _ := json.Marshal(map[string]string{"model_name": "gpt-4o"}) w := httptest.NewRecorder() diff --git a/backend/internal/handler/handler_test.go b/backend/internal/handler/handler_test.go index 74e0bb5..d705025 100644 --- a/backend/internal/handler/handler_test.go +++ b/backend/internal/handler/handler_test.go @@ -2,119 +2,34 @@ package handler import ( "bytes" - "context" "encoding/json" + "errors" "fmt" "net/http/httptest" "strings" "testing" - "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "gorm.io/gorm" "nex/backend/internal/domain" - "nex/backend/internal/provider" appErrors "nex/backend/pkg/errors" + "nex/backend/tests/mocks" ) func init() { gin.SetMode(gin.TestMode) } -// ============ Mock 实现 ============ - -type mockRoutingService struct { - result *domain.RouteResult - err error -} - -func (m *mockRoutingService) RouteByModelName(providerID, modelName string) (*domain.RouteResult, error) { - return m.result, m.err -} - -type mockStatsService struct { - err error - stats []domain.UsageStats - aggrResult []map[string]interface{} -} - -func (m *mockStatsService) Record(providerID, modelName string) error { - return m.err -} -func (m *mockStatsService) Get(providerID, modelName string, startDate, endDate *time.Time) ([]domain.UsageStats, error) { - return m.stats, nil -} -func (m *mockStatsService) Aggregate(stats []domain.UsageStats, groupBy string) []map[string]interface{} { - return m.aggrResult -} - -type mockProviderService struct { - provider *domain.Provider - providers []domain.Provider - err error -} - -func (m *mockProviderService) ListEnabledModels() ([]domain.Model, error) { - return nil, nil -} - -func (m *mockProviderService) GetModelByProviderAndName(providerID, modelName string) (*domain.Model, error) { - return nil, nil -} - -func (m *mockProviderService) Create(provider *domain.Provider) error { return m.err } -func (m *mockProviderService) Get(id string, maskKey bool) (*domain.Provider, error) { - return m.provider, m.err -} -func (m *mockProviderService) List() ([]domain.Provider, error) { return m.providers, m.err } -func (m *mockProviderService) Update(id string, updates map[string]interface{}) error { - return m.err -} -func (m *mockProviderService) Delete(id string) error { return m.err } - -type mockModelService struct { - model *domain.Model - models []domain.Model - err error -} - -func (m *mockModelService) Create(model *domain.Model) error { - if m.err == nil { - model.ID = "mock-uuid-1234" - } - return m.err -} -func (m *mockModelService) Get(id string) (*domain.Model, error) { - return m.model, m.err -} -func (m *mockModelService) List(providerID string) ([]domain.Model, error) { - return m.models, m.err -} -func (m *mockModelService) ListEnabled() ([]domain.Model, error) { - return []domain.Model{}, nil -} -func (m *mockModelService) Update(id string, updates map[string]interface{}) error { - return m.err -} -func (m *mockModelService) Delete(id string) error { return m.err } - -type mockProviderClient struct { - err error -} - -func (m *mockProviderClient) Send(ctx context.Context, spec interface{}) (interface{}, error) { - return nil, m.err -} -func (m *mockProviderClient) SendStream(ctx context.Context, spec interface{}) (<-chan provider.StreamEvent, error) { - return nil, m.err -} - -// ============ Provider Handler 测试 ============ - func TestProviderHandler_CreateProvider_MissingFields(t *testing.T) { - h := NewProviderHandler(&mockProviderService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + h := NewProviderHandler(mockSvc) body, _ := json.Marshal(map[string]string{"id": "p1"}) w := httptest.NewRecorder() @@ -127,12 +42,15 @@ func TestProviderHandler_CreateProvider_MissingFields(t *testing.T) { } func TestProviderHandler_ListProviders(t *testing.T) { - h := NewProviderHandler(&mockProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "P1"}, - {ID: "p2", Name: "P2"}, - }, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().List().Return([]domain.Provider{ + {ID: "p1", Name: "P1"}, + {ID: "p2", Name: "P2"}, + }, nil) + h := NewProviderHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -142,14 +60,17 @@ func TestProviderHandler_ListProviders(t *testing.T) { assert.Equal(t, 200, w.Code) var result []domain.Provider - json.Unmarshal(w.Body.Bytes(), &result) + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &result)) assert.Len(t, result, 2) } func TestProviderHandler_GetProvider(t *testing.T) { - h := NewProviderHandler(&mockProviderService{ - provider: &domain.Provider{ID: "p1", Name: "P1", APIKey: "***"}, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Get(gomock.Eq("p1"), gomock.Eq(true)).Return(&domain.Provider{ID: "p1", Name: "P1", APIKey: "***"}, nil) + h := NewProviderHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -160,10 +81,12 @@ func TestProviderHandler_GetProvider(t *testing.T) { assert.Equal(t, 200, w.Code) } -// ============ Model Handler 测试 ============ - func TestModelHandler_CreateModel_MissingFields(t *testing.T) { - h := NewModelHandler(&mockModelService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + h := NewModelHandler(mockSvc) body, _ := json.Marshal(map[string]string{"id": "m1"}) w := httptest.NewRecorder() @@ -176,12 +99,15 @@ func TestModelHandler_CreateModel_MissingFields(t *testing.T) { } func TestModelHandler_ListModels(t *testing.T) { - h := NewModelHandler(&mockModelService{ - models: []domain.Model{ - {ID: "m1", ProviderID: "openai", ModelName: "gpt-4"}, - {ID: "m2", ProviderID: "anthropic", ModelName: "claude-3"}, - }, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().List(gomock.Eq("")).Return([]domain.Model{ + {ID: "m1", ProviderID: "openai", ModelName: "gpt-4"}, + {ID: "m2", ProviderID: "anthropic", ModelName: "claude-3"}, + }, nil) + h := NewModelHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -198,9 +124,12 @@ func TestModelHandler_ListModels(t *testing.T) { } func TestModelHandler_GetModel_UnifiedID(t *testing.T) { - h := NewModelHandler(&mockModelService{ - model: &domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4"}, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Get(gomock.Eq("m1")).Return(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4"}, nil) + h := NewModelHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -217,7 +146,15 @@ func TestModelHandler_GetModel_UnifiedID(t *testing.T) { } func TestModelHandler_CreateModel_UnifiedID(t *testing.T) { - h := NewModelHandler(&mockModelService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).DoAndReturn(func(model *domain.Model) error { + model.ID = "mock-uuid-1234" + return nil + }) + h := NewModelHandler(mockSvc) body, _ := json.Marshal(map[string]string{ "provider_id": "openai", @@ -238,9 +175,13 @@ func TestModelHandler_CreateModel_UnifiedID(t *testing.T) { } func TestModelHandler_UpdateModel_UnifiedID(t *testing.T) { - h := NewModelHandler(&mockModelService{ - model: &domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4-turbo"}, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Update(gomock.Eq("m1"), gomock.Any()).Return(nil) + mockSvc.EXPECT().Get(gomock.Eq("m1")).Return(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4-turbo"}, nil) + h := NewModelHandler(mockSvc) body, _ := json.Marshal(map[string]interface{}{"enabled": false}) w := httptest.NewRecorder() @@ -257,14 +198,15 @@ func TestModelHandler_UpdateModel_UnifiedID(t *testing.T) { assert.Equal(t, "openai/gpt-4-turbo", result.UnifiedModelID) } -// ============ Stats Handler 测试 ============ - func TestStatsHandler_GetStats(t *testing.T) { - h := NewStatsHandler(&mockStatsService{ - stats: []domain.UsageStats{ - {ProviderID: "p1", ModelName: "gpt-4", RequestCount: 10}, - }, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockStatsService(ctrl) + mockSvc.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]domain.UsageStats{ + {ProviderID: "p1", ModelName: "gpt-4", RequestCount: 10}, + }, nil) + h := NewStatsHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -275,7 +217,11 @@ func TestStatsHandler_GetStats(t *testing.T) { } func TestStatsHandler_GetStats_InvalidDate(t *testing.T) { - h := NewStatsHandler(&mockStatsService{}) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockStatsService(ctrl) + h := NewStatsHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -286,14 +232,17 @@ func TestStatsHandler_GetStats_InvalidDate(t *testing.T) { } func TestStatsHandler_AggregateStats(t *testing.T) { - h := NewStatsHandler(&mockStatsService{ - stats: []domain.UsageStats{ - {ProviderID: "p1", RequestCount: 10}, - }, - aggrResult: []map[string]interface{}{ - {"provider_id": "p1", "request_count": 10}, - }, + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockStatsService(ctrl) + mockSvc.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]domain.UsageStats{ + {ProviderID: "p1", RequestCount: 10}, + }, nil) + mockSvc.EXPECT().Aggregate(gomock.Any(), gomock.Eq("provider")).Return([]map[string]interface{}{ + {"provider_id": "p1", "request_count": 10}, }) + h := NewStatsHandler(mockSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -303,8 +252,6 @@ func TestStatsHandler_AggregateStats(t *testing.T) { assert.Equal(t, 200, w.Code) } -// ============ writeError 测试 ============ - func TestWriteError(t *testing.T) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -333,12 +280,13 @@ func formatMapErrors(errs map[string]string) string { return "请求验证失败: " + strings.Join(parts, "; ") } -// ============ 错误类型判断测试 ============ - func TestProviderHandler_CreateProvider_DuplicatedKey(t *testing.T) { - h := NewProviderHandler(&mockProviderService{ - err: appErrors.ErrConflict, - }) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).Return(appErrors.ErrConflict) + h := NewProviderHandler(mockSvc) body, _ := json.Marshal(map[string]string{ "id": "p1", @@ -354,3 +302,158 @@ func TestProviderHandler_CreateProvider_DuplicatedKey(t *testing.T) { h.CreateProvider(c) assert.Equal(t, 409, w.Code) } + +func TestModelHandler_CreateModel_ProviderNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).Return(appErrors.ErrProviderNotFound) + h := NewModelHandler(mockSvc) + + body, _ := json.Marshal(map[string]string{ + "provider_id": "nonexistent", + "model_name": "gpt-4", + }) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/api/models", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.CreateModel(c) + assert.Equal(t, 400, w.Code) + assert.Contains(t, w.Body.String(), "供应商不存在") +} + +func TestModelHandler_CreateModel_DuplicateModel(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).Return(appErrors.ErrDuplicateModel) + h := NewModelHandler(mockSvc) + + body, _ := json.Marshal(map[string]string{ + "provider_id": "openai", + "model_name": "gpt-4", + }) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/api/models", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.CreateModel(c) + assert.Equal(t, 409, w.Code) + assert.Contains(t, w.Body.String(), "同一供应商下模型名称已存在") +} + +func TestModelHandler_CreateModel_InternalError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + mockSvc.EXPECT().Create(gomock.Any()).Return(errors.New("database error")) + h := NewModelHandler(mockSvc) + + body, _ := json.Marshal(map[string]string{ + "provider_id": "openai", + "model_name": "gpt-4", + }) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/api/models", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.CreateModel(c) + assert.Equal(t, 500, w.Code) +} + +func TestProviderHandler_UpdateProvider_NotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Update(gomock.Eq("p1"), gomock.Any()).Return(gorm.ErrRecordNotFound) + h := NewProviderHandler(mockSvc) + + body, _ := json.Marshal(map[string]interface{}{"name": "Updated"}) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "p1"}} + c.Request = httptest.NewRequest("PUT", "/api/providers/p1", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.UpdateProvider(c) + assert.Equal(t, 404, w.Code) +} + +func TestProviderHandler_UpdateProvider_ImmutableField(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Update(gomock.Eq("p1"), gomock.Any()).Return(appErrors.ErrImmutableField) + h := NewProviderHandler(mockSvc) + + body, _ := json.Marshal(map[string]interface{}{"name": "Updated"}) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "p1"}} + c.Request = httptest.NewRequest("PUT", "/api/providers/p1", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.UpdateProvider(c) + assert.Equal(t, 400, w.Code) + assert.Contains(t, w.Body.String(), "供应商 ID 不允许修改") +} + +func TestProviderHandler_UpdateProvider_InternalError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + mockSvc.EXPECT().Update(gomock.Eq("p1"), gomock.Any()).Return(errors.New("database error")) + h := NewProviderHandler(mockSvc) + + body, _ := json.Marshal(map[string]interface{}{"name": "Updated"}) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Params = gin.Params{{Key: "id", Value: "p1"}} + c.Request = httptest.NewRequest("PUT", "/api/providers/p1", bytes.NewReader(body)) + c.Request.Header.Set("Content-Type", "application/json") + + h.UpdateProvider(c) + assert.Equal(t, 500, w.Code) +} + +func TestModelHandler_CreateModel_InvalidJSON(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockModelService(ctrl) + h := NewModelHandler(mockSvc) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/api/models", bytes.NewReader([]byte("{invalid json"))) + c.Request.Header.Set("Content-Type", "application/json") + + h.CreateModel(c) + assert.Equal(t, 400, w.Code) +} + +func TestProviderHandler_CreateProvider_InvalidJSON(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockSvc := mocks.NewMockProviderService(ctrl) + h := NewProviderHandler(mockSvc) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/api/providers", bytes.NewReader([]byte("{invalid json"))) + c.Request.Header.Set("Content-Type", "application/json") + + h.CreateProvider(c) + assert.Equal(t, 400, w.Code) +} diff --git a/backend/internal/handler/proxy_handler_test.go b/backend/internal/handler/proxy_handler_test.go index d411306..6af2bb2 100644 --- a/backend/internal/handler/proxy_handler_test.go +++ b/backend/internal/handler/proxy_handler_test.go @@ -7,11 +7,11 @@ import ( "fmt" "net/http/httptest" "testing" - "time" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" "nex/backend/internal/conversion" "nex/backend/internal/conversion/anthropic" @@ -19,78 +19,13 @@ import ( "nex/backend/internal/domain" "nex/backend/internal/provider" appErrors "nex/backend/pkg/errors" + "nex/backend/tests/mocks" ) func init() { gin.SetMode(gin.TestMode) } -type mockProxyProviderClient struct { - sendFn func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) - sendStreamFn func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) -} - -func (m *mockProxyProviderClient) Send(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - if m.sendFn != nil { - return m.sendFn(ctx, spec) - } - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"id":"resp-1","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"hi"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`), - }, nil -} - -func (m *mockProxyProviderClient) SendStream(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { - if m.sendStreamFn != nil { - return m.sendStreamFn(ctx, spec) - } - ch := make(chan provider.StreamEvent, 10) - go func() { - defer close(ch) - ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"choices\":[{\"delta\":{\"content\":\"hi\"}}]}\n\n")} - ch <- provider.StreamEvent{Data: []byte("data: [DONE]\n\n")} - ch <- provider.StreamEvent{Done: true} - }() - return ch, nil -} - -type mockProxyRoutingService struct { - result *domain.RouteResult - err error -} - -func (m *mockProxyRoutingService) RouteByModelName(providerID, modelName string) (*domain.RouteResult, error) { - return m.result, m.err -} - -type mockProxyProviderService struct { - providers []domain.Provider - err error - enabledModels []domain.Model - modelByProvName *domain.Model -} - -func (m *mockProxyProviderService) ListEnabledModels() ([]domain.Model, error) { - return m.enabledModels, nil -} - -func (m *mockProxyProviderService) GetModelByProviderAndName(providerID, modelName string) (*domain.Model, error) { - return m.modelByProvName, nil -} - -func (m *mockProxyProviderService) Create(p *domain.Provider) error { return nil } -func (m *mockProxyProviderService) Get(id string, maskKey bool) (*domain.Provider, error) { return nil, nil } -func (m *mockProxyProviderService) List() ([]domain.Provider, error) { return m.providers, m.err } -func (m *mockProxyProviderService) Update(id string, updates map[string]interface{}) error { return nil } -func (m *mockProxyProviderService) Delete(id string) error { return nil } - -type mockProxyStatsService struct{} - -func (m *mockProxyStatsService) Record(providerID, modelName string) error { return nil } -func (m *mockProxyStatsService) Get(providerID, modelName string, startDate, endDate *time.Time) ([]domain.UsageStats, error) { return nil, nil } -func (m *mockProxyStatsService) Aggregate(stats []domain.UsageStats, groupBy string) []map[string]interface{} { return nil } - func setupProxyEngine(t *testing.T) *conversion.ConversionEngine { t.Helper() registry := conversion.NewMemoryRegistry() @@ -100,19 +35,26 @@ func setupProxyEngine(t *testing.T) *conversion.ConversionEngine { return engine } -func newTestProxyHandler(engine *conversion.ConversionEngine, client *mockProxyProviderClient, routingSvc *mockProxyRoutingService, providerSvc *mockProxyProviderService) *ProxyHandler { +func newTestProxyHandler(engine *conversion.ConversionEngine, client *mocks.MockProviderClient, routingSvc *mocks.MockRoutingService, providerSvc *mocks.MockProviderService, statsSvc *mocks.MockStatsService) *ProxyHandler { return NewProxyHandler( engine, client, routingSvc, providerSvc, - &mockProxyStatsService{}, + statsSvc, ) } func TestProxyHandler_HandleProxy_MissingProtocol(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, &mockProxyProviderService{}) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -123,23 +65,27 @@ func TestProxyHandler_HandleProxy_MissingProtocol(t *testing.T) { } func TestProxyHandler_HandleProxy_NonStreamSuccess(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"id":"resp-1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`{"id":"resp-1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -155,9 +101,17 @@ func TestProxyHandler_HandleProxy_NonStreamSuccess(t *testing.T) { } func TestProxyHandler_HandleProxy_RoutingError_WithBody(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(nil, appErrors.ErrModelNotFound) + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().List().Return(nil, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -169,19 +123,22 @@ func TestProxyHandler_HandleProxy_RoutingError_WithBody(t *testing.T) { } func TestProxyHandler_HandleProxy_ConversionError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return nil, context.DeadlineExceeded - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return nil, context.DeadlineExceeded + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -193,19 +150,22 @@ func TestProxyHandler_HandleProxy_ConversionError(t *testing.T) { } func TestProxyHandler_HandleProxy_ClientSendError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return nil, context.DeadlineExceeded - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return nil, context.DeadlineExceeded + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -217,27 +177,31 @@ func TestProxyHandler_HandleProxy_ClientSendError(t *testing.T) { } func TestProxyHandler_HandleProxy_StreamSuccess(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendStreamFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { - ch := make(chan provider.StreamEvent, 10) - go func() { - defer close(ch) - ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}]}\n\n")} - ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"}}]}\n\n")} - ch <- provider.StreamEvent{Data: []byte("data: [DONE]\n\n")} - ch <- provider.StreamEvent{Done: true} - }() - return ch, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().SendStream(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { + ch := make(chan provider.StreamEvent, 10) + go func() { + defer close(ch) + ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\"}}]}\n\n")} + ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"}}]}\n\n")} + ch <- provider.StreamEvent{Data: []byte("data: [DONE]\n\n")} + ch <- provider.StreamEvent{Done: true} + }() + return ch, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -251,19 +215,22 @@ func TestProxyHandler_HandleProxy_StreamSuccess(t *testing.T) { } func TestProxyHandler_HandleProxy_StreamError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendStreamFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { - return nil, context.DeadlineExceeded - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().SendStream(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { + return nil, context.DeadlineExceeded + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -275,23 +242,19 @@ func TestProxyHandler_HandleProxy_StreamError(t *testing.T) { } func TestProxyHandler_ForwardPassthrough_GET(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"object":"list","data":[{"id":"gpt-4","object":"model"}]}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{ + {ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -303,10 +266,16 @@ func TestProxyHandler_ForwardPassthrough_GET(t *testing.T) { } func TestProxyHandler_ForwardPassthrough_UnsupportedProtocol(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{} - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -318,10 +287,17 @@ func TestProxyHandler_ForwardPassthrough_UnsupportedProtocol(t *testing.T) { } func TestProxyHandler_ForwardPassthrough_NoProviders(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{providers: []domain.Provider{}} - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{}, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -329,7 +305,6 @@ func TestProxyHandler_ForwardPassthrough_NoProviders(t *testing.T) { c.Request = httptest.NewRequest("GET", "/openai/models", nil) h.HandleProxy(c) - // Models 接口现在本地聚合,返回空列表 200 assert.Equal(t, 200, w.Code) } @@ -346,22 +321,26 @@ func TestExtractHeaders(t *testing.T) { } func TestProxyHandler_HandleProxy_ProviderProtocolDefault(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Body: []byte(`{"id":"r1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Body: []byte(`{"id":"r1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -373,8 +352,15 @@ func TestProxyHandler_HandleProxy_ProviderProtocolDefault(t *testing.T) { } func TestProxyHandler_WriteConversionError_NonConversionError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, &mockProxyProviderService{}) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -385,8 +371,15 @@ func TestProxyHandler_WriteConversionError_NonConversionError(t *testing.T) { } func TestProxyHandler_WriteConversionError_ConversionError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, &mockProxyProviderService{}) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -398,22 +391,19 @@ func TestProxyHandler_WriteConversionError_ConversionError(t *testing.T) { } func TestProxyHandler_HandleProxy_EmptyBody(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Body: []byte(`{"object":"list","data":[]}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{ + {ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -425,25 +415,29 @@ func TestProxyHandler_HandleProxy_EmptyBody(t *testing.T) { } func TestProxyHandler_HandleStream_MidStreamError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendStreamFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { - ch := make(chan provider.StreamEvent, 10) - go func() { - defer close(ch) - ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"}}]}\n\n")} - ch <- provider.StreamEvent{Error: fmt.Errorf("connection reset by peer")} - }() - return ch, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().SendStream(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { + ch := make(chan provider.StreamEvent, 10) + go func() { + defer close(ch) + ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hello\"}}]}\n\n")} + ch <- provider.StreamEvent{Error: fmt.Errorf("connection reset by peer")} + }() + return ch, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -458,26 +452,30 @@ func TestProxyHandler_HandleStream_MidStreamError(t *testing.T) { } func TestProxyHandler_HandleStream_FlushOutput(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendStreamFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { - ch := make(chan provider.StreamEvent, 10) - go func() { - defer close(ch) - ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hi\"}}]}\n\n")} - ch <- provider.StreamEvent{Data: []byte("data: [DONE]\n\n")} - ch <- provider.StreamEvent{Done: true} - }() - return ch, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().SendStream(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { + ch := make(chan provider.StreamEvent, 10) + go func() { + defer close(ch) + ch <- provider.StreamEvent{Data: []byte("data: {\"id\":\"1\",\"object\":\"chat.completion.chunk\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"Hi\"}}]}\n\n")} + ch <- provider.StreamEvent{Data: []byte("data: [DONE]\n\n")} + ch <- provider.StreamEvent{Done: true} + }() + return ch, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -495,18 +493,23 @@ func TestProxyHandler_HandleStream_FlushOutput(t *testing.T) { } func TestProxyHandler_HandleStream_CreateStreamConverterError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + registry := conversion.NewMemoryRegistry() engine := conversion.NewConversionEngine(registry, nil) err := registry.Register(openai.NewAdapter()) require.NoError(t, err) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "nonexistent", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "nonexistent", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -518,17 +521,22 @@ func TestProxyHandler_HandleStream_CreateStreamConverterError(t *testing.T) { } func TestProxyHandler_HandleStream_ConvertRequestError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + registry := conversion.NewMemoryRegistry() engine := conversion.NewConversionEngine(registry, nil) require.NoError(t, registry.Register(openai.NewAdapter())) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "nonexistent", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "nonexistent", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -540,27 +548,30 @@ func TestProxyHandler_HandleStream_ConvertRequestError(t *testing.T) { } func TestProxyHandler_HandleNonStream_ConvertResponseError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + registry := conversion.NewMemoryRegistry() engine := conversion.NewConversionEngine(registry, nil) require.NoError(t, registry.Register(openai.NewAdapter())) require.NoError(t, registry.Register(anthropic.NewAdapter())) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "anthropic", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "claude-3", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`invalid json`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "anthropic", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "claude-3", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`invalid json`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -572,23 +583,27 @@ func TestProxyHandler_HandleNonStream_ConvertResponseError(t *testing.T) { } func TestProxyHandler_HandleNonStream_ResponseHeaders(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json", "X-Custom": "test-value"}, - Body: []byte(`{"id":"r1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("", "").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json", "X-Custom": "test-value"}, + Body: []byte(`{"id":"r1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}}`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -602,6 +617,9 @@ func TestProxyHandler_HandleNonStream_ResponseHeaders(t *testing.T) { } func TestProxyHandler_ForwardPassthrough_CrossProtocol(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + registry := conversion.NewMemoryRegistry() engine := conversion.NewConversionEngine(registry, nil) require.NoError(t, registry.Register(openai.NewAdapter())) @@ -609,22 +627,15 @@ func TestProxyHandler_ForwardPassthrough_CrossProtocol(t *testing.T) { anthropicAdapter := anthropic.NewAdapter() require.NoError(t, registry.Register(anthropicAdapter)) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "anthropic"}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"object":"list","data":[]}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{ + {ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -636,23 +647,19 @@ func TestProxyHandler_ForwardPassthrough_CrossProtocol(t *testing.T) { } func TestProxyHandler_ForwardPassthrough_NoBody_NoModel(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"object":"list","data":[{"id":"gpt-4","object":"model"}]}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{ + {ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -664,8 +671,15 @@ func TestProxyHandler_ForwardPassthrough_NoBody_NoModel(t *testing.T) { } func TestIsStreamRequest_EdgeCases(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, &mockProxyProviderService{}) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) tests := []struct { name string @@ -689,8 +703,15 @@ func TestIsStreamRequest_EdgeCases(t *testing.T) { } func TestProxyHandler_WriteError_RouteError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, &mockProxyProviderService{}) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -701,22 +722,19 @@ func TestProxyHandler_WriteError_RouteError(t *testing.T) { } func TestProxyHandler_HandleProxy_RouteEmptyBody_NoModel(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Body: []byte(`{"object":"list","data":[]}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{ + {ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -794,14 +812,19 @@ func TestIsStreamRequest(t *testing.T) { // ============ Models / ModelInfo 本地聚合测试 ============ func TestProxyHandler_HandleProxy_Models_LocalAggregation(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - providerSvc := &mockProxyProviderService{ - enabledModels: []domain.Model{ - {ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}, - {ID: "m2", ProviderID: "anthropic", ModelName: "claude-3", Enabled: true}, - }, - } - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, providerSvc) + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().ListEnabledModels().Return([]domain.Model{ + {ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}, + {ID: "m2", ProviderID: "anthropic", ModelName: "claude-3", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -817,17 +840,21 @@ func TestProxyHandler_HandleProxy_Models_LocalAggregation(t *testing.T) { require.True(t, ok) assert.Len(t, data, 2) - // 验证统一模型 ID 格式 first := data[0].(map[string]interface{}) assert.Equal(t, "openai/gpt-4", first["id"]) } func TestProxyHandler_HandleProxy_ModelInfo_LocalQuery(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - providerSvc := &mockProxyProviderService{ - modelByProvName: &domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}, - } - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, &mockProxyRoutingService{}, providerSvc) + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().GetModelByProviderAndName("openai", "gpt-4").Return(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}, nil) + client := mocks.NewMockProviderClient(ctrl) + routingSvc := mocks.NewMockRoutingService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -843,21 +870,25 @@ func TestProxyHandler_HandleProxy_ModelInfo_LocalQuery(t *testing.T) { } func TestProxyHandler_HandleProxy_Models_EmptySuffix_ForwardPassthrough(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Body: []byte(`{"object":"list","data":[]}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, &mockProxyRoutingService{err: appErrors.ErrModelNotFound}, providerSvc) + providerSvc := mocks.NewMockProviderService(ctrl) + providerSvc.EXPECT().List().Return([]domain.Provider{ + {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Body: []byte(`{"object":"list","data":[]}`), + }, nil + }) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName(gomock.Any(), gomock.Any()).Return(nil, appErrors.ErrModelNotFound).AnyTimes() + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -871,39 +902,40 @@ func TestProxyHandler_HandleProxy_Models_EmptySuffix_ForwardPassthrough(t *testi // ============ Smart Passthrough 统一模型 ID 路由测试 ============ func TestProxyHandler_HandleProxy_SmartPassthrough_UnifiedID(t *testing.T) { - engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "openai_p", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "openai_p", ModelName: "gpt-4", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - // 验证请求体中的 model 已被改写为上游模型名 - var req map[string]interface{} - json.Unmarshal(spec.Body, &req) - assert.Equal(t, "gpt-4", req["model"]) + ctrl := gomock.NewController(t) + defer ctrl.Finish() - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"id":"resp-1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + engine := setupProxyEngine(t) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("openai_p", "gpt-4").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "openai_p", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "openai_p", ModelName: "gpt-4", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + var req map[string]interface{} + json.Unmarshal(spec.Body, &req) + assert.Equal(t, "gpt-4", req["model"]) + + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`{"id":"resp-1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8}}`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "protocol", Value: "openai"}, {Key: "path", Value: "/chat/completions"}} - // 客户端发送统一模型 ID c.Request = httptest.NewRequest("POST", "/openai/chat/completions", bytes.NewReader([]byte(`{"model":"openai_p/gpt-4","messages":[{"role":"user","content":"hi"}]}`))) h.HandleProxy(c) assert.Equal(t, 200, w.Code) - // 验证响应中的 model 已被改写为统一模型 ID var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "openai_p/gpt-4", resp["model"]) @@ -912,70 +944,76 @@ func TestProxyHandler_HandleProxy_SmartPassthrough_UnifiedID(t *testing.T) { // ============ 跨协议统一模型 ID 路由测试 ============ func TestProxyHandler_HandleProxy_CrossProtocol_NonStream_UnifiedID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "anthropic_p", Name: "Anthropic", APIKey: "sk-test", BaseURL: "https://api.anthropic.com", Protocol: "anthropic", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "anthropic_p", ModelName: "claude-3", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"id":"msg-1","type":"message","role":"assistant","model":"claude-3","content":[{"type":"text","text":"Hello"}],"stop_reason":"end_turn","usage":{"input_tokens":5,"output_tokens":3}}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("anthropic_p", "claude-3").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "anthropic_p", Name: "Anthropic", APIKey: "sk-test", BaseURL: "https://api.anthropic.com", Protocol: "anthropic", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "anthropic_p", ModelName: "claude-3", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`{"id":"msg-1","type":"message","role":"assistant","model":"claude-3","content":[{"type":"text","text":"Hello"}],"stop_reason":"end_turn","usage":{"input_tokens":5,"output_tokens":3}}`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "protocol", Value: "openai"}, {Key: "path", Value: "/chat/completions"}} - // OpenAI 客户端使用统一模型 ID 路由到 Anthropic 供应商 c.Request = httptest.NewRequest("POST", "/openai/chat/completions", bytes.NewReader([]byte(`{"model":"anthropic_p/claude-3","messages":[{"role":"user","content":"hi"}]}`))) h.HandleProxy(c) assert.Equal(t, 200, w.Code) - // 验证跨协议转换后响应中的 model 被覆写为统一模型 ID var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "anthropic_p/claude-3", resp["model"]) } func TestProxyHandler_HandleProxy_CrossProtocol_Stream_UnifiedID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "anthropic_p", Name: "Anthropic", APIKey: "sk-test", BaseURL: "https://api.anthropic.com", Protocol: "anthropic", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "anthropic_p", ModelName: "claude-3", Enabled: true}, - }, - } - client := &mockProxyProviderClient{ - sendStreamFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { - ch := make(chan provider.StreamEvent, 10) - go func() { - defer close(ch) - ch <- provider.StreamEvent{Data: []byte(`event: message_start + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("anthropic_p", "claude-3").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "anthropic_p", Name: "Anthropic", APIKey: "sk-test", BaseURL: "https://api.anthropic.com", Protocol: "anthropic", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "anthropic_p", ModelName: "claude-3", Enabled: true}, + }, nil) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().SendStream(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { + ch := make(chan provider.StreamEvent, 10) + go func() { + defer close(ch) + ch <- provider.StreamEvent{Data: []byte(`event: message_start data: {"type":"message_start","message":{"id":"msg-1","type":"message","role":"assistant","model":"claude-3","content":[]}} `)} - ch <- provider.StreamEvent{Data: []byte(`event: content_block_delta + ch <- provider.StreamEvent{Data: []byte(`event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}} `)} - ch <- provider.StreamEvent{Data: []byte(`event: message_stop + ch <- provider.StreamEvent{Data: []byte(`event: message_stop data: {"type":"message_stop"} `)} - ch <- provider.StreamEvent{Done: true} - }() - return ch, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + ch <- provider.StreamEvent{Done: true} + }() + return ch, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) @@ -987,47 +1025,47 @@ data: {"type":"message_stop"} assert.Equal(t, "text/event-stream", w.Header().Get("Content-Type")) body := w.Body.String() - // 验证跨协议流式中 model 被覆写为统一模型 ID assert.Contains(t, body, "anthropic_p/claude-3", "跨协议流式响应中 model 应被覆写为统一模型 ID") } func TestProxyHandler_HandleProxy_SmartPassthrough_Fidelity(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{ - result: &domain.RouteResult{ - Provider: &domain.Provider{ID: "openai_p", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, - Model: &domain.Model{ID: "m1", ProviderID: "openai_p", ModelName: "gpt-4", Enabled: true}, - }, - } + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("openai_p", "gpt-4").Return(&domain.RouteResult{ + Provider: &domain.Provider{ID: "openai_p", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai", Enabled: true}, + Model: &domain.Model{ID: "m1", ProviderID: "openai_p", ModelName: "gpt-4", Enabled: true}, + }, nil) var capturedRequestBody []byte - client := &mockProxyProviderClient{ - sendFn: func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { - capturedRequestBody = spec.Body - return &conversion.HTTPResponseSpec{ - StatusCode: 200, - Headers: map[string]string{"Content-Type": "application/json"}, - Body: []byte(`{"id":"resp-1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8},"unknown_field":"preserved"}`), - }, nil - }, - } - h := newTestProxyHandler(engine, client, routingSvc, &mockProxyProviderService{}) + client := mocks.NewMockProviderClient(ctrl) + client.EXPECT().Send(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + capturedRequestBody = spec.Body + return &conversion.HTTPResponseSpec{ + StatusCode: 200, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`{"id":"resp-1","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"Hello"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":3,"total_tokens":8},"unknown_field":"preserved"}`), + }, nil + }) + providerSvc := mocks.NewMockProviderService(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + statsSvc.EXPECT().Record(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "protocol", Value: "openai"}, {Key: "path", Value: "/chat/completions"}} - // 包含未知参数,验证 Smart Passthrough 保真性 c.Request = httptest.NewRequest("POST", "/openai/chat/completions", bytes.NewReader([]byte(`{"model":"openai_p/gpt-4","messages":[{"role":"user","content":"hi"}],"custom_param":"should_be_preserved"}`))) h.HandleProxy(c) assert.Equal(t, 200, w.Code) - // 验证请求中 model 被改写为上游模型名,但未知参数保留 var reqBody map[string]interface{} require.NoError(t, json.Unmarshal(capturedRequestBody, &reqBody)) assert.Equal(t, "gpt-4", reqBody["model"], "请求中 model 应被改写为上游模型名") assert.Equal(t, "should_be_preserved", reqBody["custom_param"], "Smart Passthrough 应保留未知参数") - // 验证响应中 model 被改写为统一模型 ID,但未知参数保留 var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "openai_p/gpt-4", resp["model"], "响应中 model 应被改写为统一模型 ID") @@ -1035,19 +1073,20 @@ func TestProxyHandler_HandleProxy_SmartPassthrough_Fidelity(t *testing.T) { } func TestProxyHandler_HandleProxy_UnifiedID_ModelNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + engine := setupProxyEngine(t) - routingSvc := &mockProxyRoutingService{err: appErrors.ErrModelNotFound} - providerSvc := &mockProxyProviderService{ - providers: []domain.Provider{ - {ID: "p1", Name: "Test", APIKey: "sk-test", BaseURL: "https://api.test.com", Protocol: "openai"}, - }, - } - h := newTestProxyHandler(engine, &mockProxyProviderClient{}, routingSvc, providerSvc) + routingSvc := mocks.NewMockRoutingService(ctrl) + routingSvc.EXPECT().RouteByModelName("unknown", "model").Return(nil, appErrors.ErrModelNotFound) + providerSvc := mocks.NewMockProviderService(ctrl) + client := mocks.NewMockProviderClient(ctrl) + statsSvc := mocks.NewMockStatsService(ctrl) + h := newTestProxyHandler(engine, client, routingSvc, providerSvc, statsSvc) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Params = gin.Params{{Key: "protocol", Value: "openai"}, {Key: "path", Value: "/chat/completions"}} - // 使用统一模型 ID 格式但模型不存在 c.Request = httptest.NewRequest("POST", "/openai/chat/completions", bytes.NewReader([]byte(`{"model":"unknown/model","messages":[{"role":"user","content":"hi"}]}`))) h.HandleProxy(c) diff --git a/backend/internal/provider/client.go b/backend/internal/provider/client.go index 950f704..3dd5002 100644 --- a/backend/internal/provider/client.go +++ b/backend/internal/provider/client.go @@ -50,6 +50,7 @@ type Client struct { } // ProviderClient 供应商客户端接口 +//go:generate go run go.uber.org/mock/mockgen -source=client.go -destination=../../tests/mocks/mock_provider_client.go -package=mocks type ProviderClient interface { Send(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) SendStream(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan StreamEvent, error) diff --git a/backend/internal/repository/model_repo.go b/backend/internal/repository/model_repo.go index 929e0bd..0db38a3 100644 --- a/backend/internal/repository/model_repo.go +++ b/backend/internal/repository/model_repo.go @@ -2,6 +2,8 @@ package repository import "nex/backend/internal/domain" +//go:generate go run go.uber.org/mock/mockgen -source=model_repo.go -destination=../../tests/mocks/mock_model_repository.go -package=mocks + // ModelRepository 模型数据仓库接口 type ModelRepository interface { Create(model *domain.Model) error diff --git a/backend/internal/repository/provider_repo.go b/backend/internal/repository/provider_repo.go index 263d7a7..ef35415 100644 --- a/backend/internal/repository/provider_repo.go +++ b/backend/internal/repository/provider_repo.go @@ -2,6 +2,8 @@ package repository import "nex/backend/internal/domain" +//go:generate go run go.uber.org/mock/mockgen -source=provider_repo.go -destination=../../tests/mocks/mock_provider_repository.go -package=mocks + // ProviderRepository 供应商数据仓库接口 type ProviderRepository interface { Create(provider *domain.Provider) error @@ -9,7 +11,4 @@ type ProviderRepository interface { List() ([]domain.Provider, error) Update(id string, updates map[string]interface{}) error Delete(id string) error - // 统一模型 ID 相关方法 - ListEnabledModels() ([]domain.Model, error) - FindByProviderAndModelName(providerID, modelName string) (*domain.Model, error) } diff --git a/backend/internal/repository/provider_repo_impl.go b/backend/internal/repository/provider_repo_impl.go index d59d2a7..6ea917b 100644 --- a/backend/internal/repository/provider_repo_impl.go +++ b/backend/internal/repository/provider_repo_impl.go @@ -71,25 +71,6 @@ func (r *providerRepository) Delete(id string) error { return nil } -// ListEnabledModels 返回所有启用的模型(关联启用的供应商) -func (r *providerRepository) ListEnabledModels() ([]domain.Model, error) { - var models []domain.Model - err := r.db.Joins("JOIN providers ON providers.id = models.provider_id"). - Where("models.enabled = ? AND providers.enabled = ?", true, true). - Find(&models).Error - return models, err -} - -// FindByProviderAndModelName 按 provider_id 和 model_name 查询模型 -func (r *providerRepository) FindByProviderAndModelName(providerID, modelName string) (*domain.Model, error) { - var model domain.Model - err := r.db.Where("provider_id = ? AND model_name = ?", providerID, modelName).First(&model).Error - if err != nil { - return nil, err - } - return &model, nil -} - func toDomainProvider(p *config.Provider) domain.Provider { return domain.Provider{ ID: p.ID, diff --git a/backend/internal/repository/repository_test.go b/backend/internal/repository/repository_test.go index a0f7064..58d7bb4 100644 --- a/backend/internal/repository/repository_test.go +++ b/backend/internal/repository/repository_test.go @@ -5,28 +5,16 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" "gorm.io/gorm" - "nex/backend/internal/config" + testHelpers "nex/backend/tests" + "nex/backend/internal/domain" ) func setupTestDB(t *testing.T) *gorm.DB { t.Helper() - dir := t.TempDir() - db, err := gorm.Open(sqlite.Open(dir+"/test.db"), &gorm.Config{}) - require.NoError(t, err) - err = db.AutoMigrate(&config.Provider{}, &config.Model{}, &config.UsageStats{}) - require.NoError(t, err) - // 关闭数据库连接以便 TempDir 清理 - t.Cleanup(func() { - sqlDB, _ := db.DB() - if sqlDB != nil { - sqlDB.Close() - } - }) - return db + return testHelpers.SetupTestDB(t) } // ============ ProviderRepository 测试 ============ @@ -88,7 +76,7 @@ func TestProviderRepository_Update(t *testing.T) { db := setupTestDB(t) repo := NewProviderRepository(db) - repo.Create(&domain.Provider{ID: "p1", Name: "Old", APIKey: "key", BaseURL: "https://old.com"}) + require.NoError(t, repo.Create(&domain.Provider{ID: "p1", Name: "Old", APIKey: "key", BaseURL: "https://old.com"})) err := repo.Update("p1", map[string]interface{}{"name": "New"}) require.NoError(t, err) @@ -109,7 +97,7 @@ func TestProviderRepository_Delete(t *testing.T) { db := setupTestDB(t) repo := NewProviderRepository(db) - repo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"}) + require.NoError(t, repo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) err := repo.Delete("p1") require.NoError(t, err) @@ -129,17 +117,21 @@ func TestProviderRepository_Delete_NotFound(t *testing.T) { func TestModelRepository_Create(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) err := repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}) require.NoError(t, err) } func TestModelRepository_GetByID(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) - repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true})) result, err := repo.GetByID("m1") require.NoError(t, err) @@ -149,9 +141,11 @@ func TestModelRepository_GetByID(t *testing.T) { func TestModelRepository_FindByProviderAndModelName(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) - repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true})) result, err := repo.FindByProviderAndModelName("p1", "gpt-4") require.NoError(t, err) @@ -162,9 +156,11 @@ func TestModelRepository_FindByProviderAndModelName(t *testing.T) { func TestModelRepository_FindByProviderAndModelName_NotFound(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) - repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true})) // Wrong provider_id _, err := repo.FindByProviderAndModelName("p2", "gpt-4") @@ -181,11 +177,14 @@ func TestModelRepository_FindByProviderAndModelName_NotFound(t *testing.T) { func TestModelRepository_List(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) - repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4"}) - repo.Create(&domain.Model{ID: "m2", ProviderID: "p1", ModelName: "gpt-3.5"}) - repo.Create(&domain.Model{ID: "m3", ProviderID: "p2", ModelName: "claude-3"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p2", Name: "Test2", APIKey: "key", BaseURL: "https://test2.com"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m2", ProviderID: "p1", ModelName: "gpt-3.5"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m3", ProviderID: "p2", ModelName: "claude-3"})) all, err := repo.List("") require.NoError(t, err) @@ -246,9 +245,11 @@ func TestModelRepository_ListEnabled(t *testing.T) { func TestModelRepository_Update(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) - repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4", Enabled: true})) err := repo.Update("m1", map[string]interface{}{"enabled": false}) require.NoError(t, err) @@ -259,9 +260,11 @@ func TestModelRepository_Update(t *testing.T) { func TestModelRepository_Delete(t *testing.T) { db := setupTestDB(t) + providerRepo := NewProviderRepository(db) repo := NewModelRepository(db) - repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "Test", APIKey: "key", BaseURL: "https://test.com"})) + require.NoError(t, repo.Create(&domain.Model{ID: "m1", ProviderID: "p1", ModelName: "gpt-4"})) err := repo.Delete("m1") require.NoError(t, err) @@ -293,10 +296,32 @@ func TestStatsRepository_Query(t *testing.T) { db := setupTestDB(t) repo := NewStatsRepository(db) - repo.Record("p1", "gpt-4") + require.NoError(t, repo.Record("p1", "gpt-4")) // 注意:当前 schema 只有 date 字段有唯一约束 // 所以同一 provider + model 只能有一条记录 stats, err := repo.Query("p1", "", nil, nil) require.NoError(t, err) assert.Len(t, stats, 1) } + +func TestModelRepository_List_EmptyResult(t *testing.T) { + db := setupTestDB(t) + repo := NewModelRepository(db) + + result, err := repo.List("") + require.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result) + assert.Len(t, result, 0) +} + +func TestProviderRepository_List_EmptyResult(t *testing.T) { + db := setupTestDB(t) + repo := NewProviderRepository(db) + + result, err := repo.List() + require.NoError(t, err) + assert.NotNil(t, result) + assert.Empty(t, result) + assert.Len(t, result, 0) +} diff --git a/backend/internal/repository/stats_repo.go b/backend/internal/repository/stats_repo.go index d7c3df7..fb358bd 100644 --- a/backend/internal/repository/stats_repo.go +++ b/backend/internal/repository/stats_repo.go @@ -6,6 +6,8 @@ import ( "nex/backend/internal/domain" ) +//go:generate go run go.uber.org/mock/mockgen -source=stats_repo.go -destination=../../tests/mocks/mock_stats_repository.go -package=mocks + // StatsRepository 统计数据仓库接口 type StatsRepository interface { Record(providerID, modelName string) error diff --git a/backend/internal/service/model_service.go b/backend/internal/service/model_service.go index 032e0b4..9d7e9a0 100644 --- a/backend/internal/service/model_service.go +++ b/backend/internal/service/model_service.go @@ -2,6 +2,8 @@ package service import "nex/backend/internal/domain" +//go:generate go run go.uber.org/mock/mockgen -source=model_service.go -destination=../../tests/mocks/mock_model_service.go -package=mocks + // ModelService 模型服务接口 type ModelService interface { Create(model *domain.Model) error diff --git a/backend/internal/service/provider_service.go b/backend/internal/service/provider_service.go index 2ed3f76..7730617 100644 --- a/backend/internal/service/provider_service.go +++ b/backend/internal/service/provider_service.go @@ -2,6 +2,8 @@ package service import "nex/backend/internal/domain" +//go:generate go run go.uber.org/mock/mockgen -source=provider_service.go -destination=../../tests/mocks/mock_provider_service.go -package=mocks + // ProviderService 供应商服务接口 type ProviderService interface { Create(provider *domain.Provider) error diff --git a/backend/internal/service/routing_service.go b/backend/internal/service/routing_service.go index ca58760..f4b0070 100644 --- a/backend/internal/service/routing_service.go +++ b/backend/internal/service/routing_service.go @@ -2,6 +2,8 @@ package service import "nex/backend/internal/domain" +//go:generate go run go.uber.org/mock/mockgen -source=routing_service.go -destination=../../tests/mocks/mock_routing_service.go -package=mocks + // RoutingService 路由服务接口 type RoutingService interface { RouteByModelName(providerID, modelName string) (*domain.RouteResult, error) diff --git a/backend/internal/service/service_supplemental_test.go b/backend/internal/service/service_supplemental_test.go index 5ba9e02..f326f52 100644 --- a/backend/internal/service/service_supplemental_test.go +++ b/backend/internal/service/service_supplemental_test.go @@ -16,7 +16,7 @@ func TestProviderService_Update(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewProviderService(repo, modelRepo) - svc.Create(&domain.Provider{ID: "p1", Name: "Original", APIKey: "key", BaseURL: "https://test.com"}) + require.NoError(t, svc.Create(&domain.Provider{ID: "p1", Name: "Original", APIKey: "key", BaseURL: "https://test.com"})) err := svc.Update("p1", map[string]interface{}{"name": "Updated"}) require.NoError(t, err) @@ -42,7 +42,7 @@ func TestModelService_Get(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"})) model := &domain.Model{ProviderID: "p1", ModelName: "gpt-4"} require.NoError(t, svc.Create(model)) @@ -57,7 +57,7 @@ func TestModelService_Update(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"})) model := &domain.Model{ProviderID: "p1", ModelName: "gpt-4"} require.NoError(t, svc.Create(model)) @@ -75,7 +75,7 @@ func TestModelService_Update_ProviderID_Invalid(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"})) model := &domain.Model{ProviderID: "p1", ModelName: "gpt-4"} require.NoError(t, svc.Create(model)) @@ -89,7 +89,7 @@ func TestModelService_Delete(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "p1", Name: "P1", APIKey: "key", BaseURL: "https://test.com"})) model := &domain.Model{ProviderID: "p1", ModelName: "gpt-4"} require.NoError(t, svc.Create(model)) diff --git a/backend/internal/service/service_test.go b/backend/internal/service/service_test.go index 224db39..13b49d4 100644 --- a/backend/internal/service/service_test.go +++ b/backend/internal/service/service_test.go @@ -3,14 +3,15 @@ package service import ( "errors" "testing" + "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gorm.io/driver/sqlite" "gorm.io/gorm" - "nex/backend/internal/config" + testHelpers "nex/backend/tests" + "nex/backend/internal/domain" "nex/backend/internal/repository" appErrors "nex/backend/pkg/errors" @@ -18,18 +19,7 @@ import ( func setupServiceTestDB(t *testing.T) *gorm.DB { t.Helper() - dir := t.TempDir() - db, err := gorm.Open(sqlite.Open(dir+"/test.db"), &gorm.Config{}) - require.NoError(t, err) - err = db.AutoMigrate(&config.Provider{}, &config.Model{}, &config.UsageStats{}) - require.NoError(t, err) - t.Cleanup(func() { - sqlDB, _ := db.DB() - if sqlDB != nil { - sqlDB.Close() - } - }) - return db + return testHelpers.SetupTestDB(t) } // ============ RoutingService - RouteByModelName 测试 ============ @@ -40,9 +30,8 @@ func TestRoutingService_RouteByModelName_Success(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewRoutingService(modelRepo, providerRepo) - // 创建供应商和模型 - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com", Enabled: true}) - modelRepo.Create(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com", Enabled: true})) + require.NoError(t, modelRepo.Create(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true})) result, err := svc.RouteByModelName("openai", "gpt-4") require.NoError(t, err) @@ -66,10 +55,9 @@ func TestRoutingService_RouteByModelName_DisabledModel(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewRoutingService(modelRepo, providerRepo) - // 创建启用的供应商和禁用的模型 - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com", Enabled: true}) - modelRepo.Create(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}) - modelRepo.Update("m1", map[string]interface{}{"enabled": false}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com", Enabled: true})) + require.NoError(t, modelRepo.Create(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true})) + require.NoError(t, modelRepo.Update("m1", map[string]interface{}{"enabled": false})) _, err := svc.RouteByModelName("openai", "gpt-4") assert.True(t, errors.Is(err, appErrors.ErrModelDisabled)) @@ -81,10 +69,9 @@ func TestRoutingService_RouteByModelName_DisabledProvider(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewRoutingService(modelRepo, providerRepo) - // 创建启用的供应商和模型,然后禁用供应商 - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com", Enabled: true}) - modelRepo.Create(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true}) - providerRepo.Update("openai", map[string]interface{}{"enabled": false}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com", Enabled: true})) + require.NoError(t, modelRepo.Create(&domain.Model{ID: "m1", ProviderID: "openai", ModelName: "gpt-4", Enabled: true})) + require.NoError(t, providerRepo.Update("openai", map[string]interface{}{"enabled": false})) _, err := svc.RouteByModelName("openai", "gpt-4") assert.True(t, errors.Is(err, appErrors.ErrProviderDisabled)) @@ -98,7 +85,7 @@ func TestModelService_Create_GeneratesUUID(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"})) model := &domain.Model{ProviderID: "openai", ModelName: "gpt-4"} err := svc.Create(model) @@ -122,7 +109,7 @@ func TestModelService_Create_DuplicateModelName(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"})) model1 := &domain.Model{ProviderID: "openai", ModelName: "gpt-4"} err := svc.Create(model1) @@ -179,8 +166,8 @@ func TestModelService_Update_DuplicateModelName(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"}) - providerRepo.Create(&domain.Provider{ID: "anthropic", Name: "Anthropic", APIKey: "key", BaseURL: "https://api.anthropic.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"})) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "anthropic", Name: "Anthropic", APIKey: "key", BaseURL: "https://api.anthropic.com"})) model1 := &domain.Model{ProviderID: "openai", ModelName: "gpt-4"} err := svc.Create(model1) @@ -216,7 +203,7 @@ func TestModelService_Update_Success(t *testing.T) { modelRepo := repository.NewModelRepository(db) svc := NewModelService(modelRepo, providerRepo) - providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"}) + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"})) model := &domain.Model{ProviderID: "openai", ModelName: "gpt-4"} err := svc.Create(model) @@ -272,3 +259,223 @@ func TestProviderService_Update_Success(t *testing.T) { require.NoError(t, err) assert.Equal(t, "OpenAI Updated", updated.Name) } + +// ============ StatsService - Aggregate ByModel 测试 ============ + +func TestStatsService_Aggregate_ByModel(t *testing.T) { + tests := []struct { + name string + stats []domain.UsageStats + expected []map[string]interface{} + }{ + { + name: "multiple providers with same model name", + stats: []domain.UsageStats{ + {ProviderID: "openai", ModelName: "gpt-4", RequestCount: 10}, + {ProviderID: "azure", ModelName: "gpt-4", RequestCount: 20}, + {ProviderID: "openai", ModelName: "gpt-4", RequestCount: 5}, + }, + expected: []map[string]interface{}{ + {"provider_id": "openai", "model_name": "gpt-4", "request_count": 15}, + {"provider_id": "azure", "model_name": "gpt-4", "request_count": 20}, + }, + }, + { + name: "empty providerID", + stats: []domain.UsageStats{ + {ProviderID: "", ModelName: "gpt-4", RequestCount: 10}, + {ProviderID: "", ModelName: "gpt-4", RequestCount: 5}, + }, + expected: []map[string]interface{}{ + {"provider_id": "", "model_name": "gpt-4", "request_count": 15}, + }, + }, + { + name: "empty result set", + stats: []domain.UsageStats{}, + expected: []map[string]interface{}{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := setupServiceTestDB(t) + statsRepo := repository.NewStatsRepository(db) + svc := NewStatsService(statsRepo) + + result := svc.Aggregate(tt.stats, "model") + + assert.Len(t, result, len(tt.expected)) + for _, exp := range tt.expected { + found := false + for _, r := range result { + if r["provider_id"] == exp["provider_id"] && r["model_name"] == exp["model_name"] { + assert.Equal(t, exp["request_count"], r["request_count"]) + found = true + break + } + } + assert.True(t, found, "expected result not found: %v", exp) + } + }) + } +} + +// ============ StatsService - Aggregate ByDate 测试 ============ + +func TestStatsService_Aggregate_ByDate(t *testing.T) { + tests := []struct { + name string + stats []domain.UsageStats + expected []map[string]interface{} + }{ + { + name: "normal date grouping", + stats: []domain.UsageStats{ + {Date: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), RequestCount: 10}, + {Date: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), RequestCount: 5}, + {Date: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), RequestCount: 20}, + }, + expected: []map[string]interface{}{ + {"date": "2024-01-01", "request_count": 15}, + {"date": "2024-01-02", "request_count": 20}, + }, + }, + { + name: "zero-value time", + stats: []domain.UsageStats{ + {Date: time.Time{}, RequestCount: 10}, + {Date: time.Time{}, RequestCount: 5}, + }, + expected: []map[string]interface{}{ + {"date": "0001-01-01", "request_count": 15}, + }, + }, + { + name: "empty result set", + stats: []domain.UsageStats{}, + expected: []map[string]interface{}{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := setupServiceTestDB(t) + statsRepo := repository.NewStatsRepository(db) + svc := NewStatsService(statsRepo) + + result := svc.Aggregate(tt.stats, "date") + + assert.Len(t, result, len(tt.expected)) + for _, exp := range tt.expected { + found := false + for _, r := range result { + if r["date"] == exp["date"] { + assert.Equal(t, exp["request_count"], r["request_count"]) + found = true + break + } + } + assert.True(t, found, "expected result not found: %v", exp) + } + }) + } +} + +// ============ ProviderService - isUniqueConstraintError 测试 ============ + +func TestProviderService_isUniqueConstraintError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "nil error", + err: nil, + expected: false, + }, + { + name: "UNIQUE constraint failed", + err: errors.New("UNIQUE constraint failed"), + expected: true, + }, + { + name: "duplicate key value", + err: errors.New("duplicate key value"), + expected: true, + }, + { + name: "UNIQUE constraint case insensitive", + err: errors.New("unique constraint violation"), + expected: true, + }, + { + name: "other error", + err: errors.New("some other error"), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isUniqueConstraintError(tt.err) + assert.Equal(t, tt.expected, result) + }) + } +} + +// ============ ProviderService - List MaskAPIKey 测试 ============ + +func TestProviderService_List_MaskAPIKey(t *testing.T) { + db := setupServiceTestDB(t) + repo := repository.NewProviderRepository(db) + modelRepo := repository.NewModelRepository(db) + svc := NewProviderService(repo, modelRepo) + + provider1 := &domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "sk-1234567890", BaseURL: "https://api.openai.com"} + provider2 := &domain.Provider{ID: "anthropic", Name: "Anthropic", APIKey: "sk-anthropic1234", BaseURL: "https://api.anthropic.com"} + require.NoError(t, svc.Create(provider1)) + require.NoError(t, svc.Create(provider2)) + + providers, err := svc.List() + require.NoError(t, err) + require.Len(t, providers, 2) + + for _, p := range providers { + assert.Contains(t, p.APIKey, "***") + assert.Len(t, p.APIKey, 7) + } +} + +func TestModelService_ConcurrentCreate(t *testing.T) { + db := setupServiceTestDB(t) + providerRepo := repository.NewProviderRepository(db) + modelRepo := repository.NewModelRepository(db) + svc := NewModelService(modelRepo, providerRepo) + + require.NoError(t, providerRepo.Create(&domain.Provider{ID: "openai", Name: "OpenAI", APIKey: "key", BaseURL: "https://api.openai.com"})) + + results := make(chan error, 2) + for i := 0; i < 2; i++ { + go func() { + model := &domain.Model{ProviderID: "openai", ModelName: "gpt-4"} + results <- svc.Create(model) + }() + } + + err1 := <-results + err2 := <-results + + successCount := 0 + errorCount := 0 + for _, err := range []error{err1, err2} { + if err == nil { + successCount++ + } else { + errorCount++ + } + } + assert.Equal(t, 1, successCount) + assert.Equal(t, 1, errorCount) +} diff --git a/backend/internal/service/stats_service.go b/backend/internal/service/stats_service.go index 1d7278e..94b57d6 100644 --- a/backend/internal/service/stats_service.go +++ b/backend/internal/service/stats_service.go @@ -6,6 +6,8 @@ import ( "nex/backend/internal/domain" ) +//go:generate go run go.uber.org/mock/mockgen -source=stats_service.go -destination=../../tests/mocks/mock_stats_service.go -package=mocks + // StatsService 统计服务接口 type StatsService interface { Record(providerID, modelName string) error diff --git a/backend/tests/config/config_test.go b/backend/tests/config/config_test.go new file mode 100644 index 0000000..d15bd16 --- /dev/null +++ b/backend/tests/config/config_test.go @@ -0,0 +1,193 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "nex/backend/internal/config" +) + +func TestLoadConfig_DefaultValues(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + cfg, err := config.LoadConfigFromPath(configPath) + require.NoError(t, err) + + assert.Equal(t, 9826, cfg.Server.Port) + assert.Equal(t, 30*time.Second, cfg.Server.ReadTimeout) + assert.Equal(t, 30*time.Second, cfg.Server.WriteTimeout) + + assert.Equal(t, 10, cfg.Database.MaxIdleConns) + assert.Equal(t, 100, cfg.Database.MaxOpenConns) + assert.Equal(t, 1*time.Hour, cfg.Database.ConnMaxLifetime) + + assert.Equal(t, "info", cfg.Log.Level) + assert.Equal(t, 100, cfg.Log.MaxSize) + assert.Equal(t, 10, cfg.Log.MaxBackups) + assert.Equal(t, 30, cfg.Log.MaxAge) + assert.True(t, cfg.Log.Compress) +} + +func TestLoadConfig_EnvOverride(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + t.Setenv("NEX_SERVER_PORT", "9000") + t.Setenv("NEX_LOG_LEVEL", "debug") + t.Setenv("NEX_DATABASE_MAX_IDLE_CONNS", "20") + + cfg, err := config.LoadConfigFromPath(configPath) + require.NoError(t, err) + + assert.Equal(t, 9000, cfg.Server.Port) + assert.Equal(t, "debug", cfg.Log.Level) + assert.Equal(t, 20, cfg.Database.MaxIdleConns) +} + +func TestLoadConfig_YAMLFile(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + yamlContent := ` +server: + port: 8080 + read_timeout: 60s + write_timeout: 60s +database: + path: /custom/path.db + max_idle_conns: 5 + max_open_conns: 50 + conn_max_lifetime: 2h +log: + level: warn + path: /custom/log + max_size: 200 + max_backups: 5 + max_age: 7 + compress: false +` + err := os.WriteFile(configPath, []byte(yamlContent), 0644) + require.NoError(t, err) + + cfg, err := config.LoadConfigFromPath(configPath) + require.NoError(t, err) + + assert.Equal(t, 8080, cfg.Server.Port) + assert.Equal(t, 60*time.Second, cfg.Server.ReadTimeout) + assert.Equal(t, 60*time.Second, cfg.Server.WriteTimeout) + assert.Equal(t, "/custom/path.db", cfg.Database.Path) + assert.Equal(t, 5, cfg.Database.MaxIdleConns) + assert.Equal(t, 50, cfg.Database.MaxOpenConns) + assert.Equal(t, 2*time.Hour, cfg.Database.ConnMaxLifetime) + assert.Equal(t, "warn", cfg.Log.Level) + assert.Equal(t, "/custom/log", cfg.Log.Path) + assert.Equal(t, 200, cfg.Log.MaxSize) + assert.Equal(t, 5, cfg.Log.MaxBackups) + assert.Equal(t, 7, cfg.Log.MaxAge) + assert.False(t, cfg.Log.Compress) +} + +func TestLoadConfig_PriorityChain(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + yamlContent := ` +server: + port: 8080 +log: + level: warn +` + err := os.WriteFile(configPath, []byte(yamlContent), 0644) + require.NoError(t, err) + + t.Setenv("NEX_SERVER_PORT", "9000") + + originalArgs := os.Args + defer func() { os.Args = originalArgs }() + os.Args = []string{"test", "--server-port", "9999"} + + cfg, err := config.LoadConfigFromPath(configPath) + require.NoError(t, err) + + assert.Equal(t, 9999, cfg.Server.Port, "CLI should override ENV and YAML") + assert.Equal(t, "warn", cfg.Log.Level, "YAML value should be used when no CLI/ENV override") +} + +func TestLoadConfig_AutoCreate(t *testing.T) { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "config.yaml") + + _, err := os.Stat(configPath) + assert.True(t, os.IsNotExist(err), "config file should not exist before load") + + cfg, err := config.LoadConfigFromPath(configPath) + require.NoError(t, err) + require.NotNil(t, cfg) + + assert.Equal(t, 9826, cfg.Server.Port, "should load with default values") +} + +func TestSaveAndLoadConfig(t *testing.T) { + tmpDir := t.TempDir() + + homeDir, err := os.UserHomeDir() + require.NoError(t, err) + nexDir := filepath.Join(homeDir, ".nex") + configPath := filepath.Join(nexDir, "config.yaml") + + originalConfig, err := os.ReadFile(configPath) + if err != nil && !os.IsNotExist(err) { + require.NoError(t, err) + } + defer func() { + if originalConfig != nil { + _ = os.WriteFile(configPath, originalConfig, 0644) + } + }() + + cfg := &config.Config{ + Server: config.ServerConfig{ + Port: 7777, + ReadTimeout: 45 * time.Second, + WriteTimeout: 45 * time.Second, + }, + Database: config.DatabaseConfig{ + Path: filepath.Join(tmpDir, "test.db"), + MaxIdleConns: 15, + MaxOpenConns: 150, + ConnMaxLifetime: 2 * time.Hour, + }, + Log: config.LogConfig{ + Level: "debug", + Path: filepath.Join(tmpDir, "log"), + MaxSize: 50, + MaxBackups: 3, + MaxAge: 14, + Compress: false, + }, + } + + err = config.SaveConfig(cfg) + require.NoError(t, err) + + loaded, err := config.LoadConfig() + require.NoError(t, err) + + assert.Equal(t, cfg.Server.Port, loaded.Server.Port) + assert.Equal(t, cfg.Server.ReadTimeout, loaded.Server.ReadTimeout) + assert.Equal(t, cfg.Server.WriteTimeout, loaded.Server.WriteTimeout) + assert.Equal(t, cfg.Database.MaxIdleConns, loaded.Database.MaxIdleConns) + assert.Equal(t, cfg.Database.MaxOpenConns, loaded.Database.MaxOpenConns) + assert.Equal(t, cfg.Database.ConnMaxLifetime, loaded.Database.ConnMaxLifetime) + assert.Equal(t, cfg.Log.Level, loaded.Log.Level) + assert.Equal(t, cfg.Log.MaxSize, loaded.Log.MaxSize) + assert.Equal(t, cfg.Log.MaxBackups, loaded.Log.MaxBackups) + assert.Equal(t, cfg.Log.MaxAge, loaded.Log.MaxAge) + assert.Equal(t, cfg.Log.Compress, loaded.Log.Compress) +} diff --git a/backend/tests/helpers.go b/backend/tests/helpers.go index 19160a2..4854934 100644 --- a/backend/tests/helpers.go +++ b/backend/tests/helpers.go @@ -7,49 +7,40 @@ import ( "nex/backend/internal/config" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) -// SetupTestDB initializes an in-memory SQLite database with auto-migration. -// Uses :memory: mode with MaxOpenConns(1) to ensure all operations share the -// same connection, avoiding "database is closed" errors from connection pool. -// Enables foreign key constraints for SQLite. func SetupTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:?_foreign_keys=on"), &gorm.Config{}) - assert.NoError(t, err, "failed to open test database") + require.NoError(t, err, "failed to open test database") - // 限制为单连接,确保 :memory: 数据库不被连接池丢弃 sqlDB, err := db.DB() - assert.NoError(t, err, "failed to get underlying sql.DB") + require.NoError(t, err, "failed to get underlying sql.DB") sqlDB.SetMaxOpenConns(1) sqlDB.SetConnMaxLifetime(0) err = db.AutoMigrate(&config.Provider{}, &config.Model{}, &config.UsageStats{}) - assert.NoError(t, err, "failed to auto-migrate test database") + require.NoError(t, err, "failed to auto-migrate test database") return db } -// CleanupTestDB closes the database after a brief delay to allow async -// goroutines (e.g. stats recording) to finish. func CleanupTestDB(t *testing.T, db *gorm.DB) { t.Helper() - // 等待异步 goroutine(如 statsService.Record)完成 time.Sleep(50 * time.Millisecond) sqlDB, err := db.DB() - assert.NoError(t, err, "failed to get underlying sql.DB") + require.NoError(t, err, "failed to get underlying sql.DB") err = sqlDB.Close() - assert.NoError(t, err, "failed to close test database") + require.NoError(t, err, "failed to close test database") } -// CreateTestProvider creates a test provider and returns it. func CreateTestProvider(t *testing.T, db *gorm.DB, id string) config.Provider { t.Helper() @@ -62,13 +53,11 @@ func CreateTestProvider(t *testing.T, db *gorm.DB, id string) config.Provider { } err := db.Create(&provider).Error - assert.NoError(t, err, "failed to create test provider") + require.NoError(t, err, "failed to create test provider") return provider } -// CreateTestModel creates a test model and returns it. -// Does NOT assert on error - returns the model and error for caller to verify. func CreateTestModel(t *testing.T, db *gorm.DB, id string, providerID string, modelName string) (config.Model, error) { t.Helper() diff --git a/backend/tests/mocks/mock_model_repository.go b/backend/tests/mocks/mock_model_repository.go new file mode 100644 index 0000000..2de4d5d --- /dev/null +++ b/backend/tests/mocks/mock_model_repository.go @@ -0,0 +1,143 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: model_repo.go +// +// Generated by this command: +// +// mockgen -source=model_repo.go -destination=../../tests/mocks/mock_model_repository.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockModelRepository is a mock of ModelRepository interface. +type MockModelRepository struct { + ctrl *gomock.Controller + recorder *MockModelRepositoryMockRecorder + isgomock struct{} +} + +// MockModelRepositoryMockRecorder is the mock recorder for MockModelRepository. +type MockModelRepositoryMockRecorder struct { + mock *MockModelRepository +} + +// NewMockModelRepository creates a new mock instance. +func NewMockModelRepository(ctrl *gomock.Controller) *MockModelRepository { + mock := &MockModelRepository{ctrl: ctrl} + mock.recorder = &MockModelRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockModelRepository) EXPECT() *MockModelRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockModelRepository) Create(model *domain.Model) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", model) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockModelRepositoryMockRecorder) Create(model any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockModelRepository)(nil).Create), model) +} + +// Delete mocks base method. +func (m *MockModelRepository) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockModelRepositoryMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockModelRepository)(nil).Delete), id) +} + +// FindByProviderAndModelName mocks base method. +func (m *MockModelRepository) FindByProviderAndModelName(providerID, modelName string) (*domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindByProviderAndModelName", providerID, modelName) + ret0, _ := ret[0].(*domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindByProviderAndModelName indicates an expected call of FindByProviderAndModelName. +func (mr *MockModelRepositoryMockRecorder) FindByProviderAndModelName(providerID, modelName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindByProviderAndModelName", reflect.TypeOf((*MockModelRepository)(nil).FindByProviderAndModelName), providerID, modelName) +} + +// GetByID mocks base method. +func (m *MockModelRepository) GetByID(id string) (*domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByID", id) + ret0, _ := ret[0].(*domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByID indicates an expected call of GetByID. +func (mr *MockModelRepositoryMockRecorder) GetByID(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockModelRepository)(nil).GetByID), id) +} + +// List mocks base method. +func (m *MockModelRepository) List(providerID string) ([]domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", providerID) + ret0, _ := ret[0].([]domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockModelRepositoryMockRecorder) List(providerID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockModelRepository)(nil).List), providerID) +} + +// ListEnabled mocks base method. +func (m *MockModelRepository) ListEnabled() ([]domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListEnabled") + ret0, _ := ret[0].([]domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListEnabled indicates an expected call of ListEnabled. +func (mr *MockModelRepositoryMockRecorder) ListEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnabled", reflect.TypeOf((*MockModelRepository)(nil).ListEnabled)) +} + +// Update mocks base method. +func (m *MockModelRepository) Update(id string, updates map[string]any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", id, updates) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockModelRepositoryMockRecorder) Update(id, updates any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockModelRepository)(nil).Update), id, updates) +} diff --git a/backend/tests/mocks/mock_model_service.go b/backend/tests/mocks/mock_model_service.go new file mode 100644 index 0000000..8123123 --- /dev/null +++ b/backend/tests/mocks/mock_model_service.go @@ -0,0 +1,128 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: model_service.go +// +// Generated by this command: +// +// mockgen -source=model_service.go -destination=../../tests/mocks/mock_model_service.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockModelService is a mock of ModelService interface. +type MockModelService struct { + ctrl *gomock.Controller + recorder *MockModelServiceMockRecorder + isgomock struct{} +} + +// MockModelServiceMockRecorder is the mock recorder for MockModelService. +type MockModelServiceMockRecorder struct { + mock *MockModelService +} + +// NewMockModelService creates a new mock instance. +func NewMockModelService(ctrl *gomock.Controller) *MockModelService { + mock := &MockModelService{ctrl: ctrl} + mock.recorder = &MockModelServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockModelService) EXPECT() *MockModelServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockModelService) Create(model *domain.Model) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", model) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockModelServiceMockRecorder) Create(model any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockModelService)(nil).Create), model) +} + +// Delete mocks base method. +func (m *MockModelService) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockModelServiceMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockModelService)(nil).Delete), id) +} + +// Get mocks base method. +func (m *MockModelService) Get(id string) (*domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", id) + ret0, _ := ret[0].(*domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockModelServiceMockRecorder) Get(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockModelService)(nil).Get), id) +} + +// List mocks base method. +func (m *MockModelService) List(providerID string) ([]domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", providerID) + ret0, _ := ret[0].([]domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockModelServiceMockRecorder) List(providerID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockModelService)(nil).List), providerID) +} + +// ListEnabled mocks base method. +func (m *MockModelService) ListEnabled() ([]domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListEnabled") + ret0, _ := ret[0].([]domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListEnabled indicates an expected call of ListEnabled. +func (mr *MockModelServiceMockRecorder) ListEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnabled", reflect.TypeOf((*MockModelService)(nil).ListEnabled)) +} + +// Update mocks base method. +func (m *MockModelService) Update(id string, updates map[string]any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", id, updates) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockModelServiceMockRecorder) Update(id, updates any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockModelService)(nil).Update), id, updates) +} diff --git a/backend/tests/mocks/mock_provider_client.go b/backend/tests/mocks/mock_provider_client.go new file mode 100644 index 0000000..f8267da --- /dev/null +++ b/backend/tests/mocks/mock_provider_client.go @@ -0,0 +1,73 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.go +// +// Generated by this command: +// +// mockgen -source=client.go -destination=../../tests/mocks/mock_provider_client.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + conversion "nex/backend/internal/conversion" + provider "nex/backend/internal/provider" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockProviderClient is a mock of ProviderClient interface. +type MockProviderClient struct { + ctrl *gomock.Controller + recorder *MockProviderClientMockRecorder + isgomock struct{} +} + +// MockProviderClientMockRecorder is the mock recorder for MockProviderClient. +type MockProviderClientMockRecorder struct { + mock *MockProviderClient +} + +// NewMockProviderClient creates a new mock instance. +func NewMockProviderClient(ctrl *gomock.Controller) *MockProviderClient { + mock := &MockProviderClient{ctrl: ctrl} + mock.recorder = &MockProviderClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProviderClient) EXPECT() *MockProviderClientMockRecorder { + return m.recorder +} + +// Send mocks base method. +func (m *MockProviderClient) Send(ctx context.Context, spec conversion.HTTPRequestSpec) (*conversion.HTTPResponseSpec, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Send", ctx, spec) + ret0, _ := ret[0].(*conversion.HTTPResponseSpec) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Send indicates an expected call of Send. +func (mr *MockProviderClientMockRecorder) Send(ctx, spec any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockProviderClient)(nil).Send), ctx, spec) +} + +// SendStream mocks base method. +func (m *MockProviderClient) SendStream(ctx context.Context, spec conversion.HTTPRequestSpec) (<-chan provider.StreamEvent, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendStream", ctx, spec) + ret0, _ := ret[0].(<-chan provider.StreamEvent) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendStream indicates an expected call of SendStream. +func (mr *MockProviderClientMockRecorder) SendStream(ctx, spec any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendStream", reflect.TypeOf((*MockProviderClient)(nil).SendStream), ctx, spec) +} diff --git a/backend/tests/mocks/mock_provider_repository.go b/backend/tests/mocks/mock_provider_repository.go new file mode 100644 index 0000000..5642d4f --- /dev/null +++ b/backend/tests/mocks/mock_provider_repository.go @@ -0,0 +1,113 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: provider_repo.go +// +// Generated by this command: +// +// mockgen -source=provider_repo.go -destination=../../tests/mocks/mock_provider_repository.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockProviderRepository is a mock of ProviderRepository interface. +type MockProviderRepository struct { + ctrl *gomock.Controller + recorder *MockProviderRepositoryMockRecorder + isgomock struct{} +} + +// MockProviderRepositoryMockRecorder is the mock recorder for MockProviderRepository. +type MockProviderRepositoryMockRecorder struct { + mock *MockProviderRepository +} + +// NewMockProviderRepository creates a new mock instance. +func NewMockProviderRepository(ctrl *gomock.Controller) *MockProviderRepository { + mock := &MockProviderRepository{ctrl: ctrl} + mock.recorder = &MockProviderRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProviderRepository) EXPECT() *MockProviderRepositoryMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockProviderRepository) Create(provider *domain.Provider) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", provider) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockProviderRepositoryMockRecorder) Create(provider any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProviderRepository)(nil).Create), provider) +} + +// Delete mocks base method. +func (m *MockProviderRepository) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockProviderRepositoryMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProviderRepository)(nil).Delete), id) +} + +// GetByID mocks base method. +func (m *MockProviderRepository) GetByID(id string) (*domain.Provider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetByID", id) + ret0, _ := ret[0].(*domain.Provider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetByID indicates an expected call of GetByID. +func (mr *MockProviderRepositoryMockRecorder) GetByID(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByID", reflect.TypeOf((*MockProviderRepository)(nil).GetByID), id) +} + +// List mocks base method. +func (m *MockProviderRepository) List() ([]domain.Provider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List") + ret0, _ := ret[0].([]domain.Provider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockProviderRepositoryMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProviderRepository)(nil).List)) +} + +// Update mocks base method. +func (m *MockProviderRepository) Update(id string, updates map[string]any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", id, updates) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockProviderRepositoryMockRecorder) Update(id, updates any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProviderRepository)(nil).Update), id, updates) +} diff --git a/backend/tests/mocks/mock_provider_service.go b/backend/tests/mocks/mock_provider_service.go new file mode 100644 index 0000000..44646dc --- /dev/null +++ b/backend/tests/mocks/mock_provider_service.go @@ -0,0 +1,143 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: provider_service.go +// +// Generated by this command: +// +// mockgen -source=provider_service.go -destination=../../tests/mocks/mock_provider_service.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockProviderService is a mock of ProviderService interface. +type MockProviderService struct { + ctrl *gomock.Controller + recorder *MockProviderServiceMockRecorder + isgomock struct{} +} + +// MockProviderServiceMockRecorder is the mock recorder for MockProviderService. +type MockProviderServiceMockRecorder struct { + mock *MockProviderService +} + +// NewMockProviderService creates a new mock instance. +func NewMockProviderService(ctrl *gomock.Controller) *MockProviderService { + mock := &MockProviderService{ctrl: ctrl} + mock.recorder = &MockProviderServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockProviderService) EXPECT() *MockProviderServiceMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockProviderService) Create(provider *domain.Provider) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", provider) + ret0, _ := ret[0].(error) + return ret0 +} + +// Create indicates an expected call of Create. +func (mr *MockProviderServiceMockRecorder) Create(provider any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockProviderService)(nil).Create), provider) +} + +// Delete mocks base method. +func (m *MockProviderService) Delete(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockProviderServiceMockRecorder) Delete(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockProviderService)(nil).Delete), id) +} + +// Get mocks base method. +func (m *MockProviderService) Get(id string, maskKey bool) (*domain.Provider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", id, maskKey) + ret0, _ := ret[0].(*domain.Provider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockProviderServiceMockRecorder) Get(id, maskKey any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockProviderService)(nil).Get), id, maskKey) +} + +// GetModelByProviderAndName mocks base method. +func (m *MockProviderService) GetModelByProviderAndName(providerID, modelName string) (*domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetModelByProviderAndName", providerID, modelName) + ret0, _ := ret[0].(*domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetModelByProviderAndName indicates an expected call of GetModelByProviderAndName. +func (mr *MockProviderServiceMockRecorder) GetModelByProviderAndName(providerID, modelName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetModelByProviderAndName", reflect.TypeOf((*MockProviderService)(nil).GetModelByProviderAndName), providerID, modelName) +} + +// List mocks base method. +func (m *MockProviderService) List() ([]domain.Provider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List") + ret0, _ := ret[0].([]domain.Provider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockProviderServiceMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockProviderService)(nil).List)) +} + +// ListEnabledModels mocks base method. +func (m *MockProviderService) ListEnabledModels() ([]domain.Model, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListEnabledModels") + ret0, _ := ret[0].([]domain.Model) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListEnabledModels indicates an expected call of ListEnabledModels. +func (mr *MockProviderServiceMockRecorder) ListEnabledModels() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListEnabledModels", reflect.TypeOf((*MockProviderService)(nil).ListEnabledModels)) +} + +// Update mocks base method. +func (m *MockProviderService) Update(id string, updates map[string]any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", id, updates) + ret0, _ := ret[0].(error) + return ret0 +} + +// Update indicates an expected call of Update. +func (mr *MockProviderServiceMockRecorder) Update(id, updates any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockProviderService)(nil).Update), id, updates) +} diff --git a/backend/tests/mocks/mock_routing_service.go b/backend/tests/mocks/mock_routing_service.go new file mode 100644 index 0000000..717a8a9 --- /dev/null +++ b/backend/tests/mocks/mock_routing_service.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: routing_service.go +// +// Generated by this command: +// +// mockgen -source=routing_service.go -destination=../../tests/mocks/mock_routing_service.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockRoutingService is a mock of RoutingService interface. +type MockRoutingService struct { + ctrl *gomock.Controller + recorder *MockRoutingServiceMockRecorder + isgomock struct{} +} + +// MockRoutingServiceMockRecorder is the mock recorder for MockRoutingService. +type MockRoutingServiceMockRecorder struct { + mock *MockRoutingService +} + +// NewMockRoutingService creates a new mock instance. +func NewMockRoutingService(ctrl *gomock.Controller) *MockRoutingService { + mock := &MockRoutingService{ctrl: ctrl} + mock.recorder = &MockRoutingServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRoutingService) EXPECT() *MockRoutingServiceMockRecorder { + return m.recorder +} + +// RouteByModelName mocks base method. +func (m *MockRoutingService) RouteByModelName(providerID, modelName string) (*domain.RouteResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RouteByModelName", providerID, modelName) + ret0, _ := ret[0].(*domain.RouteResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RouteByModelName indicates an expected call of RouteByModelName. +func (mr *MockRoutingServiceMockRecorder) RouteByModelName(providerID, modelName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteByModelName", reflect.TypeOf((*MockRoutingService)(nil).RouteByModelName), providerID, modelName) +} diff --git a/backend/tests/mocks/mock_stats_repository.go b/backend/tests/mocks/mock_stats_repository.go new file mode 100644 index 0000000..d031185 --- /dev/null +++ b/backend/tests/mocks/mock_stats_repository.go @@ -0,0 +1,71 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: stats_repo.go +// +// Generated by this command: +// +// mockgen -source=stats_repo.go -destination=../../tests/mocks/mock_stats_repository.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + time "time" + + gomock "go.uber.org/mock/gomock" +) + +// MockStatsRepository is a mock of StatsRepository interface. +type MockStatsRepository struct { + ctrl *gomock.Controller + recorder *MockStatsRepositoryMockRecorder + isgomock struct{} +} + +// MockStatsRepositoryMockRecorder is the mock recorder for MockStatsRepository. +type MockStatsRepositoryMockRecorder struct { + mock *MockStatsRepository +} + +// NewMockStatsRepository creates a new mock instance. +func NewMockStatsRepository(ctrl *gomock.Controller) *MockStatsRepository { + mock := &MockStatsRepository{ctrl: ctrl} + mock.recorder = &MockStatsRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStatsRepository) EXPECT() *MockStatsRepositoryMockRecorder { + return m.recorder +} + +// Query mocks base method. +func (m *MockStatsRepository) Query(providerID, modelName string, startDate, endDate *time.Time) ([]domain.UsageStats, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Query", providerID, modelName, startDate, endDate) + ret0, _ := ret[0].([]domain.UsageStats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Query indicates an expected call of Query. +func (mr *MockStatsRepositoryMockRecorder) Query(providerID, modelName, startDate, endDate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockStatsRepository)(nil).Query), providerID, modelName, startDate, endDate) +} + +// Record mocks base method. +func (m *MockStatsRepository) Record(providerID, modelName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Record", providerID, modelName) + ret0, _ := ret[0].(error) + return ret0 +} + +// Record indicates an expected call of Record. +func (mr *MockStatsRepositoryMockRecorder) Record(providerID, modelName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Record", reflect.TypeOf((*MockStatsRepository)(nil).Record), providerID, modelName) +} diff --git a/backend/tests/mocks/mock_stats_service.go b/backend/tests/mocks/mock_stats_service.go new file mode 100644 index 0000000..72c310c --- /dev/null +++ b/backend/tests/mocks/mock_stats_service.go @@ -0,0 +1,85 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: stats_service.go +// +// Generated by this command: +// +// mockgen -source=stats_service.go -destination=../../tests/mocks/mock_stats_service.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + domain "nex/backend/internal/domain" + reflect "reflect" + time "time" + + gomock "go.uber.org/mock/gomock" +) + +// MockStatsService is a mock of StatsService interface. +type MockStatsService struct { + ctrl *gomock.Controller + recorder *MockStatsServiceMockRecorder + isgomock struct{} +} + +// MockStatsServiceMockRecorder is the mock recorder for MockStatsService. +type MockStatsServiceMockRecorder struct { + mock *MockStatsService +} + +// NewMockStatsService creates a new mock instance. +func NewMockStatsService(ctrl *gomock.Controller) *MockStatsService { + mock := &MockStatsService{ctrl: ctrl} + mock.recorder = &MockStatsServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStatsService) EXPECT() *MockStatsServiceMockRecorder { + return m.recorder +} + +// Aggregate mocks base method. +func (m *MockStatsService) Aggregate(stats []domain.UsageStats, groupBy string) []map[string]any { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Aggregate", stats, groupBy) + ret0, _ := ret[0].([]map[string]any) + return ret0 +} + +// Aggregate indicates an expected call of Aggregate. +func (mr *MockStatsServiceMockRecorder) Aggregate(stats, groupBy any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Aggregate", reflect.TypeOf((*MockStatsService)(nil).Aggregate), stats, groupBy) +} + +// Get mocks base method. +func (m *MockStatsService) Get(providerID, modelName string, startDate, endDate *time.Time) ([]domain.UsageStats, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", providerID, modelName, startDate, endDate) + ret0, _ := ret[0].([]domain.UsageStats) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockStatsServiceMockRecorder) Get(providerID, modelName, startDate, endDate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStatsService)(nil).Get), providerID, modelName, startDate, endDate) +} + +// Record mocks base method. +func (m *MockStatsService) Record(providerID, modelName string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Record", providerID, modelName) + ret0, _ := ret[0].(error) + return ret0 +} + +// Record indicates an expected call of Record. +func (mr *MockStatsServiceMockRecorder) Record(providerID, modelName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Record", reflect.TypeOf((*MockStatsService)(nil).Record), providerID, modelName) +} diff --git a/openspec/specs/test-coverage/spec.md b/openspec/specs/test-coverage/spec.md index 6892da6..e747e62 100644 --- a/openspec/specs/test-coverage/spec.md +++ b/openspec/specs/test-coverage/spec.md @@ -31,6 +31,57 @@ - **THEN** SHALL 测试请求转换、响应转换、流式转换 - **THEN** SHALL 验证转换的准确性和完整性 +#### Scenario: config 加载管道集成测试 + +- **WHEN** 运行 config 加载管道的集成测试 +- **THEN** SHALL 验证 LoadConfigFromPath 正确加载默认值 +- **THEN** SHALL 验证环境变量(`NEX_` 前缀)覆盖默认值 +- **THEN** SHALL 验证 YAML 配置文件正确读取 +- **THEN** SHALL 验证优先级链:CLI 参数 > 环境变量 > YAML 文件 > 默认值 +- **THEN** SHALL 验证首次启动自动创建配置文件 +- **THEN** SHALL 验证 SaveConfig 后重新 LoadConfig 数据一致 + +#### Scenario: handler 错误分支测试 + +- **WHEN** 运行 handler 层的单元测试 +- **THEN** SHALL 覆盖 ModelHandler.CreateModel 的所有错误分支:ErrProviderNotFound(400)、ErrDuplicateModel(409)、通用错误(500) +- **THEN** SHALL 覆盖 ProviderHandler.UpdateProvider 的所有错误分支:ErrRecordNotFound(404)、ErrImmutableField(400)、通用错误(500) +- **THEN** SHALL 验证每个错误分支返回正确的 HTTP 状态码和错误响应格式 + +#### Scenario: service 聚合逻辑测试 + +- **WHEN** 运行 service 层的单元测试 +- **THEN** SHALL 覆盖 StatsService.Aggregate 的所有分组模式:byProvider、byModel、byDate +- **THEN** SHALL 验证 aggregateByModel 正确拼接和拆分 providerID/modelName key +- **THEN** SHALL 验证 aggregateByDate 正确格式化日期并聚合 +- **THEN** SHALL 覆盖空结果集、同名模型不同 provider 等边界场景 + +#### Scenario: provider service 工具方法测试 + +- **WHEN** 运行 provider service 的单元测试 +- **THEN** SHALL 验证 isUniqueConstraintError 正确识别 SQLite 唯一约束冲突错误消息 +- **THEN** SHALL 验证 List 方法对每个 provider 调用 MaskAPIKey + +#### Scenario: engine 降级路径测试 + +- **WHEN** 运行 conversion engine 的单元测试 +- **THEN** SHALL 验证 convertEmbeddingBody 在 decode 失败时返回原始 body(passthrough) +- **THEN** SHALL 验证 convertRerankBody 在 decode 失败时返回原始 body(passthrough) +- **THEN** SHALL 验证降级过程不 panic、不返回空 body + +#### Scenario: openai decoder 边界场景测试 + +- **WHEN** 运行 openai decoder 的单元测试 +- **THEN** SHALL 覆盖 assistant message content 为 JSON 数组格式的解析(text/refusal 类型) +- **THEN** SHALL 验证 decodeContentParts 正确提取文本内容和拒绝消息 + +#### Scenario: 业务逻辑负面测试 + +- **WHEN** 运行业务逻辑负面测试 +- **THEN** SHALL 覆盖 JSON 格式错误请求体的处理 +- **THEN** SHALL 覆盖并发创建相同 provider + model 的重复检测 +- **THEN** SHALL 覆盖空结果集查询的正确处理 + ### Requirement: 建立集成测试体系 系统 SHALL 建立集成测试体系,覆盖 API 端到端流程。 @@ -63,26 +114,28 @@ #### Scenario: 测试数据库初始化 - **WHEN** 编写需要数据库的测试 -- **THEN** SHALL 提供测试数据库初始化函数 -- **THEN** SHALL 使用临时数据库文件 +- **THEN** SHALL 提供统一的测试数据库初始化函数 `SetupTestDB` +- **THEN** SHALL 统一使用 SQLite `:memory:` + `MaxOpenConns(1)` 策略 - **THEN** SHALL 在测试结束后自动清理 +- **THEN** 所有测试包 SHALL 通过 `tests.SetupTestDB()` 获取测试数据库,不允许各自独立实现 #### Scenario: Mock 工具 - **WHEN** 编写需要 Mock 的测试 -- **THEN** SHALL 提供 Mock 接口实现 -- **THEN** SHALL 支持常见 Mock 场景 -- **THEN** SHALL 易于使用和扩展 +- **THEN** SHALL 使用 mockgen 自动生成 mock 实现 +- **THEN** SHALL 在接口定义文件中使用 `//go:generate` 注解标注生成命令 +- **THEN** 生成的 mock SHALL 放置在 `tests/mocks/` 目录下 +- **THEN** SHALL 覆盖 service 和 repository 接口的 mock 生成 ### Requirement: 达到测试覆盖率目标 -系统 SHALL 达到 > 80% 的测试覆盖率。 +系统 SHALL 达到 > 85% 的测试覆盖率。 #### Scenario: 总体覆盖率 - **WHEN** 运行所有测试并生成覆盖率报告 -- **THEN** 总体覆盖率 SHALL 大于 80% -- **THEN** 核心包覆盖率 SHALL 大于 85% +- **THEN** 总体覆盖率 SHALL 大于 85% +- **THEN** 核心包(config、service、handler、conversion、repository)覆盖率 SHALL 大于 85% #### Scenario: 覆盖率报告生成 @@ -102,6 +155,14 @@ - **THEN** SHALL 显示测试结果 - **THEN** SHALL 在测试失败时返回非零退出码 +#### Scenario: 分类测试命令 + +- **WHEN** 执行 `make test-unit` 命令 +- **THEN** SHALL 仅运行 `./internal/...` 和 `./pkg/...` 下的单元测试 + +- **WHEN** 执行 `make test-integration` 命令 +- **THEN** SHALL 仅运行 `./tests/...` 下的集成测试 + #### Scenario: 覆盖率检查命令 - **WHEN** 执行 `make test-coverage` 命令 @@ -170,3 +231,33 @@ - **WHEN** 在 frontend/ 目录执行 E2E 测试命令 - **THEN** SHALL 启动 Playwright 运行 E2E 测试 - **THEN** SHALL 在测试失败时返回非零退出码 + +### Requirement: 清理 ProviderRepository 死代码 + +系统 SHALL 移除 ProviderRepository 中未被调用的重复方法。 + +#### Scenario: 移除死代码方法 + +- **WHEN** 审查 ProviderRepository 接口 +- **THEN** SHALL 移除 `ListEnabledModels()` 方法声明和实现 +- **THEN** SHALL 移除 `FindByProviderAndModelName()` 方法声明和实现 +- **THEN** SHALL 确保所有现有调用者通过 ModelRepository 访问等效功能 +- **THEN** SHALL 不影响任何运行时行为 + +### Requirement: 使用 mockgen 生成 mock + +系统 SHALL 使用 mockgen 为接口自动生成 mock 实现,替代手写 mock。 + +#### Scenario: mock 生成配置 + +- **WHEN** 在接口定义文件中添加 `//go:generate mockgen` 注解 +- **THEN** SHALL 为 `ProviderService`、`ModelService`、`RoutingService`、`StatsService` 接口生成 mock +- **THEN** SHALL 为 `ModelRepository`、`ProviderRepository`、`StatsRepository` 接口生成 mock +- **THEN** SHALL 为 `ProviderClient` 接口生成 mock +- **THEN** 生成的 mock SHALL 输出到 `tests/mocks/` 目录 + +#### Scenario: 替换手写 mock + +- **WHEN** mockgen 生成的 mock 就绪 +- **THEN** handler 测试中的手写 mock SHALL 被替换为生成的 mock +- **THEN** 所有测试 SHALL 继续通过,行为不变