feat: add login page
This commit is contained in:
parent
513a290031
commit
4828386370
|
|
@ -1,4 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings" defaultProject="true" />
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
10
Makefile
10
Makefile
|
|
@ -21,6 +21,16 @@ gen-doc: # 生成 Swagger 文檔
|
|||
# go-doc openapi --api ./generate/api/gateway.api --filename gateway.json --host dev-api.truheart.com.tw --basepath /api/v1
|
||||
go-doc -a generate/api/gateway.api -d ./ -f gateway -s openapi3.0
|
||||
|
||||
.PHONY: mock-gen
|
||||
mock-gen: # 建立 mock 資料
|
||||
mockgen -source=./pkg/member/domain/repository/account.go -destination=./pkg/member/mock/repository/account.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/repository/account_uid.go -destination=./pkg/member/mock/repository/account_uid.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/repository/auto_id.go -destination=./pkg/member/mock/repository/auto_id.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/repository/user.go -destination=./pkg/member/mock/repository/user.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/repository/verify_code.go -destination=./pkg/member/mock/repository/verify_code.go -package=mock
|
||||
mockgen -source=./pkg/member/domain/usecase/generate_uid.go -destination=./pkg/member/mock/usecase/generate_uid.go -package=mock
|
||||
|
||||
@echo "Generate mock files successfully"
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: # 格式優化
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
mode: set
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:18.104,21.9 2 1
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:21.9,23.3 1 1
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:26.2,27.16 2 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:27.16,29.3 1 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:31.2,31.35 1 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:34.104,36.16 2 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:36.16,38.3 1 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:41.2,42.16 2 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:42.16,44.3 1 0
|
||||
backend/pkg/library/mongo/custom_mongo_decimal.go:47.2,49.12 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:18.171,20.16 2 1
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:20.16,22.3 1 1
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:24.2,29.8 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:32.84,34.2 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:36.66,38.2 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:40.66,42.2 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:45.137,48.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:48.27,49.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:49.17,51.28 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:51.28,53.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:54.4,54.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:54.26,56.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:57.4,57.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:57.23,59.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:60.4,60.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:64.2,65.16 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:65.16,67.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:69.2,69.46 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:69.46,71.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:73.2,73.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:77.127,78.57 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:78.57,81.28 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:81.28,82.18 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:82.18,84.29 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:84.29,86.6 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:87.5,87.27 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:87.27,89.6 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:90.5,90.24 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:90.24,92.6 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:93.5,93.30 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:93.30,95.6 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:96.5,96.24 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:96.24,98.6 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:99.5,99.24 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:99.24,101.6 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:102.5,102.45 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:106.3,106.63 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:111.145,114.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:114.27,115.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:115.17,117.28 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:117.28,119.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:120.4,120.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:120.26,122.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:123.4,123.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:123.23,125.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:126.4,126.29 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:126.29,128.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:129.4,129.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:129.23,131.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:132.4,132.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:136.2,136.87 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:136.87,138.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:140.2,140.30 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:144.160,147.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:147.27,148.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:148.17,150.28 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:150.28,152.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:153.4,153.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:153.26,155.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:156.4,156.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:156.23,158.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:159.4,159.29 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:159.29,161.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:162.4,162.33 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:162.33,164.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:165.4,165.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:165.23,167.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:168.4,168.25 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:168.25,170.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:171.4,171.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:175.2,175.101 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:175.101,177.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:179.2,179.30 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:183.156,186.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:186.27,187.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:187.17,189.43 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:189.43,191.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:192.4,192.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:192.26,194.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:195.4,195.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:199.2,200.16 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:200.16,202.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:204.2,204.45 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:204.45,206.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:208.2,208.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:212.156,215.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:215.27,216.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:216.17,218.31 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:218.31,220.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:221.4,221.43 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:221.43,223.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:224.4,224.28 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:224.28,226.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:227.4,227.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:227.26,229.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:230.4,230.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:230.23,232.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:233.4,233.25 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:233.25,235.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:236.4,236.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:240.2,241.16 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:241.16,243.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:245.2,245.45 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:245.45,247.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:249.2,249.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:253.164,256.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:256.27,257.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:257.17,259.31 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:259.31,261.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:262.4,262.43 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:262.43,264.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:265.4,265.28 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:265.28,267.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:268.4,268.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:268.26,270.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:271.4,271.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:271.23,273.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:274.4,274.25 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:274.25,276.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:277.4,277.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:281.2,282.16 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:282.16,284.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:286.2,286.49 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:286.49,288.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:290.2,290.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:294.159,297.27 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:297.27,298.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:298.17,300.31 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:300.31,302.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:303.4,303.43 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:303.43,305.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:306.4,306.28 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:306.28,308.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:309.4,309.26 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:309.26,311.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:312.4,312.23 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:312.23,314.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:315.4,315.25 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:315.25,317.5 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:318.4,318.44 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:322.2,323.16 2 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:323.16,325.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:327.2,327.45 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:327.45,329.3 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:331.2,331.17 1 0
|
||||
backend/pkg/library/mongo/doc-db-with-cache.go:337.77,339.2 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:23.100,25.23 2 1
|
||||
backend/pkg/library/mongo/doc-db.go:25.23,31.3 1 1
|
||||
backend/pkg/library/mongo/doc-db.go:33.2,41.16 3 1
|
||||
backend/pkg/library/mongo/doc-db.go:41.16,43.3 1 1
|
||||
backend/pkg/library/mongo/doc-db.go:44.2,46.43 3 0
|
||||
backend/pkg/library/mongo/doc-db.go:46.43,50.3 3 0
|
||||
backend/pkg/library/mongo/doc-db.go:52.2,57.16 5 0
|
||||
backend/pkg/library/mongo/doc-db.go:57.16,59.3 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:61.2,68.16 4 0
|
||||
backend/pkg/library/mongo/doc-db.go:68.16,70.3 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:71.2,75.8 2 0
|
||||
backend/pkg/library/mongo/doc-db.go:78.101,85.16 5 0
|
||||
backend/pkg/library/mongo/doc-db.go:85.16,87.3 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:90.115,98.16 5 0
|
||||
backend/pkg/library/mongo/doc-db.go:98.16,100.3 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:103.112,104.29 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:104.29,108.3 2 0
|
||||
backend/pkg/library/mongo/doc-db.go:110.2,115.16 5 0
|
||||
backend/pkg/library/mongo/doc-db.go:115.16,117.3 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:120.52,122.2 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:124.144,126.29 2 0
|
||||
backend/pkg/library/mongo/doc-db.go:126.29,130.3 3 0
|
||||
backend/pkg/library/mongo/doc-db.go:131.2,131.21 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:131.21,133.3 1 0
|
||||
backend/pkg/library/mongo/doc-db.go:134.2,139.14 3 0
|
||||
backend/pkg/library/mongo/option.go:19.56,20.40 1 1
|
||||
backend/pkg/library/mongo/option.go:20.40,22.32 2 0
|
||||
backend/pkg/library/mongo/option.go:22.32,25.4 2 0
|
||||
backend/pkg/library/mongo/option.go:26.3,26.26 1 0
|
||||
backend/pkg/library/mongo/option.go:31.40,37.2 1 1
|
||||
backend/pkg/library/mongo/option.go:39.44,40.43 1 1
|
||||
backend/pkg/library/mongo/option.go:40.43,45.3 4 0
|
||||
91
go.mod
91
go.mod
|
|
@ -2,49 +2,112 @@ module backend
|
|||
|
||||
go 1.25.1
|
||||
|
||||
require github.com/zeromicro/go-zero v1.9.0
|
||||
require (
|
||||
code.30cm.net/digimon/library-go/errs v1.2.14
|
||||
code.30cm.net/digimon/library-go/mongo v0.0.9
|
||||
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5
|
||||
github.com/alicebob/miniredis/v2 v2.35.0
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/testcontainers/testcontainers-go v0.39.0
|
||||
github.com/zeromicro/go-zero v1.9.0
|
||||
go.mongodb.org/mongo-driver v1.17.1
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0
|
||||
go.uber.org/mock v0.4.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grafana/pyroscope-go v1.2.4 // indirect
|
||||
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.1.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/user v0.4.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/openzipkin/zipkin-go v0.4.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_golang v1.21.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.12.1 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yuin/gopher-lua v1.1.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
244
go.sum
244
go.sum
|
|
@ -1,23 +1,76 @@
|
|||
code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4OqLsaSKW0T4RJU=
|
||||
code.30cm.net/digimon/library-go/errs v1.2.14/go.mod h1:Hs4v7SbXNggDVBGXSYsFMjkii1qLF+rugrIpWePN4/o=
|
||||
code.30cm.net/digimon/library-go/mongo v0.0.9 h1:fPciIE5B85tXpLg8aeVQqKVbLnfpVAk9xbMu7pE2tVw=
|
||||
code.30cm.net/digimon/library-go/mongo v0.0.9/go.mod h1:KBVKz/Ci5IheI77BgZxPUeKkaGvDy8fV8EDHSCOLIO4=
|
||||
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5 h1:szWsI0K+1iEHmc/AtKx+5c7tDIc1AZdStvT0tVza1pg=
|
||||
code.30cm.net/digimon/library-go/utils/invited_code v1.2.5/go.mod h1:eHmWpbX6N6KXQ2xaY71uj5bwfzTaNL8pQc2njYo5Gj0=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
|
||||
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
|
||||
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
|
||||
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
|
||||
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/pyroscope-go v1.2.4 h1:B22GMXz+O0nWLatxLuaP7o7L9dvP0clLvIpmeEQQM0Q=
|
||||
|
|
@ -28,27 +81,61 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1
|
|||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
|
||||
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
|
||||
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
|
||||
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
||||
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
|
||||
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
|
|
@ -59,8 +146,16 @@ github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ
|
|||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
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/redis/go-redis/v9 v9.12.1 h1:k5iquqv27aBtnTm2tIkROUDp8JBXhXZIVu1InSgvovg=
|
||||
github.com/redis/go-redis/v9 v9.12.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
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/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
|
@ -68,16 +163,46 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
|||
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.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=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
|
||||
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zeromicro/go-zero v1.9.0 h1:hlVtQCSHPszQdcwZTawzGwTej1G2mhHybYzMRLuwCt4=
|
||||
github.com/zeromicro/go-zero v1.9.0/go.mod h1:TMyCxiaOjLQ3YxyYlJrejaQZF40RlzQ3FVvFu5EbcV4=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
|
||||
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0 h1:sh55yOXA2vUjW1QYw/2tRlHSQViwDyPnW61AwpZ4rtU=
|
||||
go.mongodb.org/mongo-driver/v2 v2.3.0/go.mod h1:jHeEDJHJq7tm6ZF45Issun9dbogjfnPySb1vXA7EeAI=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
|
||||
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
|
||||
|
|
@ -90,32 +215,89 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDO
|
|||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
|
||||
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
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=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/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.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
|
||||
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
@ -128,5 +310,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package mongo
|
||||
|
||||
import "time"
|
||||
|
||||
type Conf struct {
|
||||
Schema string
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Database string
|
||||
ReplicaName string
|
||||
MaxStaleness time.Duration
|
||||
MaxPoolSize uint64
|
||||
MinPoolSize uint64
|
||||
MaxConnIdleTime time.Duration
|
||||
Compressors []string
|
||||
EnableStandardReadWriteSplitMode bool
|
||||
ConnectTimeoutMs int64
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConf_DefaultValues(t *testing.T) {
|
||||
conf := &Conf{}
|
||||
|
||||
// Test default values
|
||||
if conf.Schema != "" {
|
||||
t.Errorf("Expected empty Schema, got %s", conf.Schema)
|
||||
}
|
||||
if conf.User != "" {
|
||||
t.Errorf("Expected empty User, got %s", conf.User)
|
||||
}
|
||||
if conf.Password != "" {
|
||||
t.Errorf("Expected empty Password, got %s", conf.Password)
|
||||
}
|
||||
if conf.Host != "" {
|
||||
t.Errorf("Expected empty Host, got %s", conf.Host)
|
||||
}
|
||||
if conf.Database != "" {
|
||||
t.Errorf("Expected empty Database, got %s", conf.Database)
|
||||
}
|
||||
if conf.ReplicaName != "" {
|
||||
t.Errorf("Expected empty ReplicaName, got %s", conf.ReplicaName)
|
||||
}
|
||||
if conf.MaxStaleness != 0 {
|
||||
t.Errorf("Expected zero MaxStaleness, got %v", conf.MaxStaleness)
|
||||
}
|
||||
if conf.MaxPoolSize != 0 {
|
||||
t.Errorf("Expected zero MaxPoolSize, got %d", conf.MaxPoolSize)
|
||||
}
|
||||
if conf.MinPoolSize != 0 {
|
||||
t.Errorf("Expected zero MinPoolSize, got %d", conf.MinPoolSize)
|
||||
}
|
||||
if conf.MaxConnIdleTime != 0 {
|
||||
t.Errorf("Expected zero MaxConnIdleTime, got %v", conf.MaxConnIdleTime)
|
||||
}
|
||||
if conf.Compressors != nil {
|
||||
t.Errorf("Expected nil Compressors, got %v", conf.Compressors)
|
||||
}
|
||||
if conf.EnableStandardReadWriteSplitMode {
|
||||
t.Errorf("Expected false EnableStandardReadWriteSplitMode, got %v", conf.EnableStandardReadWriteSplitMode)
|
||||
}
|
||||
if conf.ConnectTimeoutMs != 0 {
|
||||
t.Errorf("Expected zero ConnectTimeoutMs, got %d", conf.ConnectTimeoutMs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConf_WithValues(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Schema: "mongodb",
|
||||
User: "testuser",
|
||||
Password: "testpass",
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
ReplicaName: "testreplica",
|
||||
MaxStaleness: 30 * time.Second,
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 10,
|
||||
MaxConnIdleTime: 5 * time.Minute,
|
||||
Compressors: []string{"snappy", "zlib"},
|
||||
EnableStandardReadWriteSplitMode: true,
|
||||
ConnectTimeoutMs: 5000,
|
||||
}
|
||||
|
||||
// Test set values
|
||||
if conf.Schema != "mongodb" {
|
||||
t.Errorf("Expected 'mongodb' Schema, got %s", conf.Schema)
|
||||
}
|
||||
if conf.User != "testuser" {
|
||||
t.Errorf("Expected 'testuser' User, got %s", conf.User)
|
||||
}
|
||||
if conf.Password != "testpass" {
|
||||
t.Errorf("Expected 'testpass' Password, got %s", conf.Password)
|
||||
}
|
||||
if conf.Host != "localhost:27017" {
|
||||
t.Errorf("Expected 'localhost:27017' Host, got %s", conf.Host)
|
||||
}
|
||||
if conf.Database != "testdb" {
|
||||
t.Errorf("Expected 'testdb' Database, got %s", conf.Database)
|
||||
}
|
||||
if conf.ReplicaName != "testreplica" {
|
||||
t.Errorf("Expected 'testreplica' ReplicaName, got %s", conf.ReplicaName)
|
||||
}
|
||||
if conf.MaxStaleness != 30*time.Second {
|
||||
t.Errorf("Expected 30s MaxStaleness, got %v", conf.MaxStaleness)
|
||||
}
|
||||
if conf.MaxPoolSize != 100 {
|
||||
t.Errorf("Expected 100 MaxPoolSize, got %d", conf.MaxPoolSize)
|
||||
}
|
||||
if conf.MinPoolSize != 10 {
|
||||
t.Errorf("Expected 10 MinPoolSize, got %d", conf.MinPoolSize)
|
||||
}
|
||||
if conf.MaxConnIdleTime != 5*time.Minute {
|
||||
t.Errorf("Expected 5m MaxConnIdleTime, got %v", conf.MaxConnIdleTime)
|
||||
}
|
||||
if len(conf.Compressors) != 2 {
|
||||
t.Errorf("Expected 2 Compressors, got %d", len(conf.Compressors))
|
||||
}
|
||||
if conf.Compressors[0] != "snappy" || conf.Compressors[1] != "zlib" {
|
||||
t.Errorf("Expected ['snappy', 'zlib'] Compressors, got %v", conf.Compressors)
|
||||
}
|
||||
if !conf.EnableStandardReadWriteSplitMode {
|
||||
t.Errorf("Expected true EnableStandardReadWriteSplitMode, got %v", conf.EnableStandardReadWriteSplitMode)
|
||||
}
|
||||
if conf.ConnectTimeoutMs != 5000 {
|
||||
t.Errorf("Expected 5000 ConnectTimeoutMs, got %d", conf.ConnectTimeoutMs)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/syncx"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
const (
|
||||
authenticationStringTemplate = "%s:%s@"
|
||||
connectionStringTemplate = "%s://%s%s"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotFound is an alias of mongo.ErrNoDocuments.
|
||||
ErrNotFound = mongo.ErrNoDocuments
|
||||
|
||||
// can't use one SingleFlight per conn, because multiple conns may share the same cache key.
|
||||
singleFlight = syncx.NewSingleFlight()
|
||||
stats = cache.NewStat("monc")
|
||||
)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type MgoDecimal struct{}
|
||||
|
||||
var (
|
||||
_ bson.ValueEncoder = &MgoDecimal{}
|
||||
_ bson.ValueDecoder = &MgoDecimal{}
|
||||
)
|
||||
|
||||
func (dc *MgoDecimal) EncodeValue(_ bson.EncodeContext, w bson.ValueWriter, value reflect.Value) error {
|
||||
// TODO 待確認是否有非decimal.Decimal type而導致error的場景
|
||||
dec, ok := value.Interface().(decimal.Decimal)
|
||||
if !ok {
|
||||
return fmt.Errorf("value %v to encode is not of type decimal.Decimal", value)
|
||||
}
|
||||
|
||||
// Convert decimal.Decimal to bson.Decimal128.
|
||||
primDec, err := bson.ParseDecimal128(dec.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting decimal.Decimal %v to bson.Decimal128 error: %w", dec, err)
|
||||
}
|
||||
|
||||
return w.WriteDecimal128(primDec)
|
||||
}
|
||||
|
||||
func (dc *MgoDecimal) DecodeValue(_ bson.DecodeContext, r bson.ValueReader, value reflect.Value) error {
|
||||
primDec, err := r.ReadDecimal128()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading bson.Decimal128 from ValueReader error: %w", err)
|
||||
}
|
||||
|
||||
// Convert bson.Decimal128 to decimal.Decimal.
|
||||
dec, err := decimal.NewFromString(primDec.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("converting bson.Decimal128 %v to decimal.Decimal error: %w", primDec, err)
|
||||
}
|
||||
|
||||
// set as decimal.Decimal type
|
||||
value.Set(reflect.ValueOf(dec))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestMgoDecimal_InterfaceCompliance(t *testing.T) {
|
||||
encoder := &MgoDecimal{}
|
||||
decoder := &MgoDecimal{}
|
||||
|
||||
// Test that they implement the required interfaces
|
||||
var _ bson.ValueEncoder = encoder
|
||||
var _ bson.ValueDecoder = decoder
|
||||
|
||||
// Test that they can be used in TypeCodec
|
||||
codec := TypeCodec{
|
||||
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||
Encoder: encoder,
|
||||
Decoder: decoder,
|
||||
}
|
||||
|
||||
if codec.Encoder != encoder {
|
||||
t.Error("Expected encoder to be set correctly")
|
||||
}
|
||||
if codec.Decoder != decoder {
|
||||
t.Error("Expected decoder to be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMgoDecimal_EncodeValue_InvalidType(t *testing.T) {
|
||||
encoder := &MgoDecimal{}
|
||||
|
||||
// Test with invalid type
|
||||
value := reflect.ValueOf("not a decimal")
|
||||
|
||||
err := encoder.EncodeValue(bson.EncodeContext{}, nil, value)
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid type, got nil")
|
||||
}
|
||||
|
||||
expectedErr := "value not a decimal to encode is not of type decimal.Decimal"
|
||||
if err.Error() != expectedErr {
|
||||
t.Errorf("Expected error '%s', got '%s'", expectedErr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Test decimal conversion functions
|
||||
func TestDecimalConversion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"0", "0"},
|
||||
{"123.45", "123.45"},
|
||||
{"-123.45", "-123.45"},
|
||||
{"0.000001", "0.000001"},
|
||||
{"9999999999999999999.999999999999999", "9999999999999999999.999999999999999"},
|
||||
{"-9999999999999999999.999999999999999", "-9999999999999999999.999999999999999"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
// Test decimal to string conversion
|
||||
dec, err := decimal.NewFromString(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decimal from %s: %v", tc.input, err)
|
||||
}
|
||||
|
||||
if dec.String() != tc.expected {
|
||||
t.Errorf("Expected %s, got %s", tc.expected, dec.String())
|
||||
}
|
||||
|
||||
// Test BSON decimal128 conversion
|
||||
primDec, err := bson.ParseDecimal128(dec.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err)
|
||||
}
|
||||
|
||||
if primDec.String() != tc.expected {
|
||||
t.Errorf("Expected %s, got %s", tc.expected, primDec.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test error cases
|
||||
func TestDecimalConversionErrors(t *testing.T) {
|
||||
invalidCases := []string{
|
||||
"invalid",
|
||||
"not a number",
|
||||
"",
|
||||
"123.45.67",
|
||||
"abc123",
|
||||
}
|
||||
|
||||
for _, invalid := range invalidCases {
|
||||
t.Run(invalid, func(t *testing.T) {
|
||||
_, err := decimal.NewFromString(invalid)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for invalid decimal string: %s", invalid)
|
||||
}
|
||||
|
||||
_, err = bson.ParseDecimal128(invalid)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for invalid decimal128 string: %s", invalid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test edge cases for decimal values
|
||||
func TestDecimalEdgeCases(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
value decimal.Decimal
|
||||
expected string
|
||||
}{
|
||||
{"zero", decimal.Zero, "0"},
|
||||
{"positive small", decimal.NewFromFloat(0.000001), "0.000001"},
|
||||
{"negative small", decimal.NewFromFloat(-0.000001), "-0.000001"},
|
||||
{"positive large", decimal.NewFromInt(999999999999999), "999999999999999"},
|
||||
{"negative large", decimal.NewFromInt(-999999999999999), "-999999999999999"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Test conversion to BSON Decimal128
|
||||
primDec, err := bson.ParseDecimal128(tc.value.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse decimal128 from %s: %v", tc.value.String(), err)
|
||||
}
|
||||
|
||||
// Test conversion back to decimal
|
||||
dec, err := decimal.NewFromString(primDec.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err)
|
||||
}
|
||||
|
||||
if !dec.Equal(tc.value) {
|
||||
t.Errorf("Round trip failed: original=%s, result=%s", tc.value.String(), dec.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test error handling in encoder
|
||||
func TestMgoDecimal_EncoderErrors(t *testing.T) {
|
||||
encoder := &MgoDecimal{}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
value interface{}
|
||||
}{
|
||||
{"string", "not a decimal"},
|
||||
{"int", 123},
|
||||
{"float", 123.45},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
value := reflect.ValueOf(tc.value)
|
||||
err := encoder.EncodeValue(bson.EncodeContext{}, nil, value)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for type %T, got nil", tc.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test decimal precision
|
||||
func TestDecimalPrecision(t *testing.T) {
|
||||
testCases := []string{
|
||||
"0.1",
|
||||
"0.01",
|
||||
"0.001",
|
||||
"0.0001",
|
||||
"0.00001",
|
||||
"0.000001",
|
||||
"0.0000001",
|
||||
"0.00000001",
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
dec, err := decimal.NewFromString(tc)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decimal from %s: %v", tc, err)
|
||||
}
|
||||
|
||||
// Test conversion to BSON Decimal128
|
||||
primDec, err := bson.ParseDecimal128(dec.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err)
|
||||
}
|
||||
|
||||
// Test conversion back to decimal
|
||||
result, err := decimal.NewFromString(primDec.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err)
|
||||
}
|
||||
|
||||
if !result.Equal(dec) {
|
||||
t.Errorf("Precision lost: original=%s, result=%s", dec.String(), result.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test large numbers
|
||||
func TestDecimalLargeNumbers(t *testing.T) {
|
||||
testCases := []string{
|
||||
"1000000000000000",
|
||||
"10000000000000000",
|
||||
"100000000000000000",
|
||||
"1000000000000000000",
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
dec, err := decimal.NewFromString(tc)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decimal from %s: %v", tc, err)
|
||||
}
|
||||
|
||||
// Test conversion to BSON Decimal128
|
||||
primDec, err := bson.ParseDecimal128(dec.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse decimal128 from %s: %v", dec.String(), err)
|
||||
}
|
||||
|
||||
// Test conversion back to decimal
|
||||
result, err := decimal.NewFromString(primDec.String())
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create decimal from %s: %v", primDec.String(), err)
|
||||
}
|
||||
|
||||
if !result.Equal(dec) {
|
||||
t.Errorf("Large number lost: original=%s, result=%s", dec.String(), result.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkMgoDecimal_ParseDecimal128(b *testing.B) {
|
||||
dec := decimal.NewFromFloat(123.45)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = bson.ParseDecimal128(dec.String())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMgoDecimal_DecimalFromString(b *testing.B) {
|
||||
primDec, _ := bson.ParseDecimal128("123.45")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = decimal.NewFromString(primDec.String())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMgoDecimal_RoundTrip(b *testing.B) {
|
||||
dec := decimal.NewFromFloat(123.45)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
primDec, _ := bson.ParseDecimal128(dec.String())
|
||||
_, _ = decimal.NewFromString(primDec.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type DocumentDBWithCache struct {
|
||||
DocumentDBUseCase
|
||||
Cache cache.Cache
|
||||
}
|
||||
|
||||
func MustDocumentDBWithCache(conf *Conf, collection string, cacheConf cache.CacheConf, dbOpts []mon.Option, cacheOpts []cache.Option) (DocumentDBWithCacheUseCase, error) {
|
||||
documentDB, err := NewDocumentDB(conf, collection, dbOpts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize DocumentDB: %w", err)
|
||||
}
|
||||
|
||||
c := MustModelCache(cacheConf, cacheOpts...)
|
||||
|
||||
return &DocumentDBWithCache{
|
||||
DocumentDBUseCase: documentDB,
|
||||
Cache: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (dc *DocumentDBWithCache) DelCache(ctx context.Context, keys ...string) error {
|
||||
return dc.Cache.DelCtx(ctx, keys...)
|
||||
}
|
||||
|
||||
func (dc *DocumentDBWithCache) GetCache(key string, v any) error {
|
||||
return dc.Cache.Get(key, v)
|
||||
}
|
||||
|
||||
func (dc *DocumentDBWithCache) SetCache(key string, v any) error {
|
||||
return dc.Cache.Set(key, v)
|
||||
}
|
||||
|
||||
// DeleteOne deletes a single document and invalidates cache
|
||||
func (dc *DocumentDBWithCache) DeleteOne(ctx context.Context, key string, filter any, opts ...*options.DeleteOneOptions) (int64, error) {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.DeleteOneOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.DeleteOne()
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
val, err := dc.GetClient().DeleteOne(ctx, filter, listerOpts...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err := dc.DelCache(ctx, key); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// FindOne finds a single document with cache support
|
||||
func (dc *DocumentDBWithCache) FindOne(ctx context.Context, key string, v, filter any, opts ...*options.FindOneOptions) error {
|
||||
return dc.Cache.TakeCtx(ctx, v, key, func(v any) error {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.FindOneOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.FindOne()
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
if opt.Projection != nil {
|
||||
builder.SetProjection(opt.Projection)
|
||||
}
|
||||
if opt.Skip != nil {
|
||||
builder.SetSkip(*opt.Skip)
|
||||
}
|
||||
if opt.Sort != nil {
|
||||
builder.SetSort(opt.Sort)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
return dc.GetClient().FindOne(ctx, v, filter, listerOpts...)
|
||||
})
|
||||
}
|
||||
|
||||
// FindOneAndDelete finds and deletes a single document with cache invalidation
|
||||
func (dc *DocumentDBWithCache) FindOneAndDelete(ctx context.Context, key string, v, filter any, opts ...*options.FindOneAndDeleteOptions) error {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.FindOneAndDeleteOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.FindOneAndDelete()
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
if opt.Projection != nil {
|
||||
builder.SetProjection(opt.Projection)
|
||||
}
|
||||
if opt.Sort != nil {
|
||||
builder.SetSort(opt.Sort)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
if err := dc.GetClient().FindOneAndDelete(ctx, v, filter, listerOpts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dc.DelCache(ctx, key)
|
||||
}
|
||||
|
||||
// FindOneAndReplace finds and replaces a single document with cache invalidation
|
||||
func (dc *DocumentDBWithCache) FindOneAndReplace(ctx context.Context, key string, v, filter, replacement any, opts ...*options.FindOneAndReplaceOptions) error {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.FindOneAndReplaceOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.FindOneAndReplace()
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
if opt.Projection != nil {
|
||||
builder.SetProjection(opt.Projection)
|
||||
}
|
||||
if opt.ReturnDocument != nil {
|
||||
builder.SetReturnDocument(*opt.ReturnDocument)
|
||||
}
|
||||
if opt.Sort != nil {
|
||||
builder.SetSort(opt.Sort)
|
||||
}
|
||||
if opt.Upsert != nil {
|
||||
builder.SetUpsert(*opt.Upsert)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
if err := dc.GetClient().FindOneAndReplace(ctx, v, filter, replacement, listerOpts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dc.DelCache(ctx, key)
|
||||
}
|
||||
|
||||
// InsertOne inserts a single document and invalidates cache
|
||||
func (dc *DocumentDBWithCache) InsertOne(ctx context.Context, key string, document any, opts ...*options.InsertOneOptions) (*mongo.InsertOneResult, error) {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.InsertOneOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.InsertOne()
|
||||
if opt.BypassDocumentValidation != nil {
|
||||
builder.SetBypassDocumentValidation(*opt.BypassDocumentValidation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := dc.GetClient().Collection.InsertOne(ctx, document, listerOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = dc.DelCache(ctx, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateByID updates a document by ID and invalidates cache
|
||||
func (dc *DocumentDBWithCache) UpdateByID(ctx context.Context, key string, id, update any, opts ...*options.UpdateOneOptions) (*mongo.UpdateResult, error) {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.UpdateOneOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.UpdateOne()
|
||||
if opt.ArrayFilters != nil {
|
||||
builder.SetArrayFilters(opt.ArrayFilters)
|
||||
}
|
||||
if opt.BypassDocumentValidation != nil {
|
||||
builder.SetBypassDocumentValidation(*opt.BypassDocumentValidation)
|
||||
}
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
if opt.Upsert != nil {
|
||||
builder.SetUpsert(*opt.Upsert)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := dc.GetClient().Collection.UpdateByID(ctx, id, update, listerOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = dc.DelCache(ctx, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateMany updates multiple documents and invalidates cache
|
||||
func (dc *DocumentDBWithCache) UpdateMany(ctx context.Context, keys []string, filter, update any, opts ...*options.UpdateManyOptions) (*mongo.UpdateResult, error) {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.UpdateManyOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.UpdateMany()
|
||||
if opt.ArrayFilters != nil {
|
||||
builder.SetArrayFilters(opt.ArrayFilters)
|
||||
}
|
||||
if opt.BypassDocumentValidation != nil {
|
||||
builder.SetBypassDocumentValidation(*opt.BypassDocumentValidation)
|
||||
}
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
if opt.Upsert != nil {
|
||||
builder.SetUpsert(*opt.Upsert)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := dc.GetClient().Collection.UpdateMany(ctx, filter, update, listerOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = dc.DelCache(ctx, keys...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateOne updates a single document and invalidates cache
|
||||
func (dc *DocumentDBWithCache) UpdateOne(ctx context.Context, key string, filter, update any, opts ...*options.UpdateOneOptions) (*mongo.UpdateResult, error) {
|
||||
// Convert options to Builder format
|
||||
var listerOpts []options.Lister[options.UpdateOneOptions]
|
||||
for _, opt := range opts {
|
||||
if opt != nil {
|
||||
builder := options.UpdateOne()
|
||||
if opt.ArrayFilters != nil {
|
||||
builder.SetArrayFilters(opt.ArrayFilters)
|
||||
}
|
||||
if opt.BypassDocumentValidation != nil {
|
||||
builder.SetBypassDocumentValidation(*opt.BypassDocumentValidation)
|
||||
}
|
||||
if opt.Collation != nil {
|
||||
builder.SetCollation(opt.Collation)
|
||||
}
|
||||
if opt.Comment != nil {
|
||||
builder.SetComment(opt.Comment)
|
||||
}
|
||||
if opt.Hint != nil {
|
||||
builder.SetHint(opt.Hint)
|
||||
}
|
||||
if opt.Upsert != nil {
|
||||
builder.SetUpsert(*opt.Upsert)
|
||||
}
|
||||
listerOpts = append(listerOpts, builder)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := dc.GetClient().Collection.UpdateOne(ctx, filter, update, listerOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = dc.DelCache(ctx, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ========================
|
||||
|
||||
// MustModelCache returns a cache cluster.
|
||||
func MustModelCache(conf cache.CacheConf, opts ...cache.Option) cache.Cache {
|
||||
return cache.New(conf, singleFlight, stats, mongo.ErrNoDocuments, opts...)
|
||||
}
|
||||
|
|
@ -0,0 +1,364 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
func TestDocumentDBWithCache_MustDocumentDBWithCache(t *testing.T) {
|
||||
// Test with valid config
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
// This will panic if MongoDB is not available, so we need to handle it
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Logf("Expected panic in test environment: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
if err != nil {
|
||||
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDBWithCache to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_CacheOperations(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
ctx := context.Background()
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test cache operations
|
||||
key := "test-key"
|
||||
value := "test-value"
|
||||
|
||||
// Test SetCache
|
||||
err = db.SetCache(key, value)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set cache: %v", err)
|
||||
}
|
||||
|
||||
// Test GetCache
|
||||
var cachedValue string
|
||||
err = db.GetCache(key, &cachedValue)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get cache: %v", err)
|
||||
}
|
||||
|
||||
if cachedValue != value {
|
||||
t.Errorf("Expected cached value %s, got %s", value, cachedValue)
|
||||
}
|
||||
|
||||
// Test DelCache
|
||||
err = db.DelCache(ctx, key)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to delete cache: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_CRUDOperations(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
ctx := context.Background()
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test data
|
||||
testDoc := bson.M{
|
||||
"name": "test",
|
||||
"value": 123,
|
||||
"price": decimal.NewFromFloat(99.99),
|
||||
}
|
||||
|
||||
// Test InsertOne
|
||||
result, err := db.InsertOne(ctx, collection, testDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to insert document: %v", err)
|
||||
}
|
||||
|
||||
insertedID := result.InsertedID
|
||||
if insertedID == nil {
|
||||
t.Error("Expected inserted ID to be non-nil")
|
||||
}
|
||||
|
||||
// Test FindOne
|
||||
var foundDoc bson.M
|
||||
err = db.FindOne(ctx, collection, bson.M{"_id": insertedID}, &foundDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find document: %v", err)
|
||||
}
|
||||
|
||||
if foundDoc["name"] != "test" {
|
||||
t.Errorf("Expected name 'test', got %v", foundDoc["name"])
|
||||
}
|
||||
|
||||
// Test UpdateOne
|
||||
update := bson.M{"$set": bson.M{"value": 456}}
|
||||
updateResult, err := db.UpdateOne(ctx, collection, bson.M{"_id": insertedID}, update)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update document: %v", err)
|
||||
}
|
||||
|
||||
if updateResult.ModifiedCount != 1 {
|
||||
t.Errorf("Expected 1 modified document, got %d", updateResult.ModifiedCount)
|
||||
}
|
||||
|
||||
// Test UpdateByID
|
||||
updateByID := bson.M{"$set": bson.M{"value": 789}}
|
||||
updateByIDResult, err := db.UpdateByID(ctx, collection, insertedID, updateByID)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update document by ID: %v", err)
|
||||
}
|
||||
|
||||
if updateByIDResult.ModifiedCount != 1 {
|
||||
t.Errorf("Expected 1 modified document, got %d", updateByIDResult.ModifiedCount)
|
||||
}
|
||||
|
||||
// Test UpdateMany
|
||||
updateMany := bson.M{"$set": bson.M{"updated": true}}
|
||||
updateManyResult, err := db.UpdateMany(ctx, []string{collection}, bson.M{"_id": insertedID}, updateMany)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to update many documents: %v", err)
|
||||
}
|
||||
|
||||
if updateManyResult.ModifiedCount != 1 {
|
||||
t.Errorf("Expected 1 modified document, got %d", updateManyResult.ModifiedCount)
|
||||
}
|
||||
|
||||
// Test FindOneAndReplace
|
||||
replacement := bson.M{
|
||||
"name": "replaced",
|
||||
"value": 999,
|
||||
"price": decimal.NewFromFloat(199.99),
|
||||
}
|
||||
|
||||
var replacedDoc bson.M
|
||||
err = db.FindOneAndReplace(ctx, collection, bson.M{"_id": insertedID}, replacement, &replacedDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find and replace document: %v", err)
|
||||
}
|
||||
|
||||
// Test FindOneAndDelete
|
||||
var deletedDoc bson.M
|
||||
err = db.FindOneAndDelete(ctx, collection, bson.M{"_id": insertedID}, &deletedDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find and delete document: %v", err)
|
||||
}
|
||||
|
||||
// Test DeleteOne
|
||||
deleteResult, err := db.DeleteOne(ctx, collection, bson.M{"_id": insertedID})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to delete document: %v", err)
|
||||
}
|
||||
|
||||
if deleteResult != 0 { // Should be 0 since we already deleted it
|
||||
t.Errorf("Expected 0 deleted documents, got %d", deleteResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_MustModelCache(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test that we got a valid DocumentDBWithCache
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDBWithCache to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_ErrorHandling(t *testing.T) {
|
||||
// Test with invalid config
|
||||
invalidConf := &Conf{
|
||||
Host: "invalid-host:99999",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
_, err := MustDocumentDBWithCache(invalidConf, collection, cacheConf, nil, nil)
|
||||
|
||||
// This should fail
|
||||
if err == nil {
|
||||
t.Error("Expected error with invalid host, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_ContextHandling(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
// Test with timeout context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
|
||||
// Use ctx to avoid unused variable warning
|
||||
_ = ctx
|
||||
|
||||
if err != nil {
|
||||
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDBWithCache to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_WithDecimalValues(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
ctx := context.Background()
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test with decimal values
|
||||
testDoc := bson.M{
|
||||
"name": "decimal-test",
|
||||
"price": decimal.NewFromFloat(123.45),
|
||||
"amount": decimal.NewFromFloat(999.99),
|
||||
}
|
||||
|
||||
// Insert document with decimal values
|
||||
result, err := db.InsertOne(ctx, collection, testDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to insert document with decimal values: %v", err)
|
||||
}
|
||||
|
||||
insertedID := result.InsertedID
|
||||
|
||||
// Find document with decimal values
|
||||
var foundDoc bson.M
|
||||
err = db.FindOne(ctx, collection, bson.M{"_id": insertedID}, &foundDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find document with decimal values: %v", err)
|
||||
}
|
||||
|
||||
// Verify decimal values
|
||||
if foundDoc["name"] != "decimal-test" {
|
||||
t.Errorf("Expected name 'decimal-test', got %v", foundDoc["name"])
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_, err = db.DeleteOne(ctx, collection, bson.M{"_id": insertedID})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to clean up document: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDBWithCache_WithObjectID(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
collection := "testcollection"
|
||||
cacheConf := cache.CacheConf{}
|
||||
|
||||
ctx := context.Background()
|
||||
db, err := MustDocumentDBWithCache(conf, collection, cacheConf, nil, nil)
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test with ObjectID
|
||||
objectID := bson.NewObjectID()
|
||||
testDoc := bson.M{
|
||||
"_id": objectID,
|
||||
"name": "objectid-test",
|
||||
"value": 123,
|
||||
}
|
||||
|
||||
// Insert document with ObjectID
|
||||
result, err := db.InsertOne(ctx, collection, testDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to insert document with ObjectID: %v", err)
|
||||
}
|
||||
|
||||
insertedID := result.InsertedID
|
||||
|
||||
// Verify ObjectID
|
||||
if insertedID != objectID {
|
||||
t.Errorf("Expected ObjectID %v, got %v", objectID, insertedID)
|
||||
}
|
||||
|
||||
// Find document by ObjectID
|
||||
var foundDoc bson.M
|
||||
err = db.FindOne(ctx, collection, bson.M{"_id": objectID}, &foundDoc)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to find document by ObjectID: %v", err)
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_, err = db.DeleteOne(ctx, collection, bson.M{"_id": objectID})
|
||||
if err != nil {
|
||||
t.Errorf("Failed to clean up document: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/readpref"
|
||||
)
|
||||
|
||||
type DocumentDB struct {
|
||||
Mon *mon.Model
|
||||
}
|
||||
|
||||
func NewDocumentDB(config *Conf, collection string, opts ...mon.Option) (DocumentDBUseCase, error) {
|
||||
authenticationURI := ""
|
||||
if config.User != "" {
|
||||
authenticationURI = fmt.Sprintf(
|
||||
authenticationStringTemplate,
|
||||
config.User,
|
||||
config.Password,
|
||||
)
|
||||
}
|
||||
|
||||
connectionURI := fmt.Sprintf(
|
||||
connectionStringTemplate,
|
||||
config.Schema,
|
||||
authenticationURI,
|
||||
config.Host,
|
||||
)
|
||||
|
||||
connectUri, err := url.Parse(connectionURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse connection URI: %w", err)
|
||||
}
|
||||
printConnectUri := connectUri.String()
|
||||
findIndexAt := strings.Index(connectUri.String(), "@")
|
||||
if findIndexAt > -1 && config.User != "" {
|
||||
prefixIndex := len(config.Schema) + 3 + len(config.User)
|
||||
connectUriStr := connectUri.String()
|
||||
printConnectUri = fmt.Sprintf("%s:*****%s", connectUriStr[:prefixIndex], connectUriStr[findIndexAt:])
|
||||
}
|
||||
// 初始化選項
|
||||
intOpt := InitMongoOptions(*config)
|
||||
opts = append(opts, intOpt)
|
||||
|
||||
logx.Infof("[DocumentDB] Try to connect document db `%s`", printConnectUri)
|
||||
client, err := mon.NewModel(connectionURI, config.Database, collection, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(
|
||||
context.Background(),
|
||||
time.Duration(config.ConnectTimeoutMs)*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// Force a connection to verify our connection string
|
||||
err = client.Database().Client().Ping(ctx, readpref.SecondaryPreferred())
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Failed to ping cluster: %s", err))
|
||||
}
|
||||
logx.Infof("[DocumentDB] Connected to DocumentDB!")
|
||||
|
||||
return &DocumentDB{
|
||||
Mon: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (document *DocumentDB) PopulateIndex(ctx context.Context, key string, sort int32, unique bool) {
|
||||
c := document.Mon.Collection
|
||||
opts := options.CreateIndexes()
|
||||
index := document.yieldIndexModel(
|
||||
[]string{key}, []int32{sort}, unique, nil,
|
||||
)
|
||||
_, err := c.Indexes().CreateOne(ctx, index, opts)
|
||||
if err != nil {
|
||||
logx.Errorf("[DocumentDb] Ensure Index Failed, %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (document *DocumentDB) PopulateTTLIndex(ctx context.Context, key string, sort int32, unique bool, ttl int32) {
|
||||
c := document.Mon.Collection
|
||||
opts := options.CreateIndexes()
|
||||
index := document.yieldIndexModel(
|
||||
[]string{key}, []int32{sort}, unique,
|
||||
options.Index().SetExpireAfterSeconds(ttl),
|
||||
)
|
||||
_, err := c.Indexes().CreateOne(ctx, index, opts)
|
||||
if err != nil {
|
||||
logx.Errorf("[DocumentDb] Ensure TTL Index Failed, %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (document *DocumentDB) PopulateMultiIndex(ctx context.Context, keys []string, sorts []int32, unique bool) {
|
||||
if len(keys) != len(sorts) {
|
||||
logx.Infof("[DocumentDb] Ensure Indexes Failed Please provide some item length of keys/sorts")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c := document.Mon.Collection
|
||||
opts := options.CreateIndexes()
|
||||
index := document.yieldIndexModel(keys, sorts, unique, nil)
|
||||
|
||||
_, err := c.Indexes().CreateOne(ctx, index, opts)
|
||||
if err != nil {
|
||||
logx.Errorf("[DocumentDb] Ensure TTL Index Failed, %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (document *DocumentDB) GetClient() *mon.Model {
|
||||
return document.Mon
|
||||
}
|
||||
|
||||
func (document *DocumentDB) yieldIndexModel(keys []string, sorts []int32, unique bool, indexOpt *options.IndexOptionsBuilder) mongo.IndexModel {
|
||||
SetKeysDoc := bson.D{}
|
||||
for index, _ := range keys {
|
||||
key := keys[index]
|
||||
sort := sorts[index]
|
||||
SetKeysDoc = append(SetKeysDoc, bson.E{Key: key, Value: sort})
|
||||
}
|
||||
if indexOpt == nil {
|
||||
indexOpt = options.Index()
|
||||
}
|
||||
indexOpt.SetUnique(unique)
|
||||
index := mongo.IndexModel{
|
||||
Keys: SetKeysDoc,
|
||||
Options: indexOpt,
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewDocumentDB(t *testing.T) {
|
||||
// Test with valid config
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
// Note: This will fail in test environment without MongoDB running
|
||||
// but we can test the error handling and basic structure
|
||||
if err == nil {
|
||||
t.Log("MongoDB connection successful (MongoDB is running)")
|
||||
|
||||
// Test basic properties
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDB to be non-nil")
|
||||
}
|
||||
|
||||
// Test GetClient
|
||||
client := db.GetClient()
|
||||
if client == nil {
|
||||
t.Error("Expected client to be non-nil")
|
||||
}
|
||||
|
||||
// Test that we got a valid DocumentDB
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDB to be non-nil")
|
||||
}
|
||||
} else {
|
||||
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDB_PopulateIndex(t *testing.T) {
|
||||
// Test with mock data
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test index creation
|
||||
ctx := context.Background()
|
||||
db.PopulateIndex(ctx, "field1", 1, false)
|
||||
}
|
||||
|
||||
func TestDocumentDB_PopulateTTLIndex(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test TTL index creation
|
||||
ctx := context.Background()
|
||||
ttl := int32(3600) // 1 hour
|
||||
db.PopulateTTLIndex(ctx, "expireAt", 1, false, ttl)
|
||||
}
|
||||
|
||||
func TestDocumentDB_PopulateMultiIndex(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test multiple index creation
|
||||
ctx := context.Background()
|
||||
keys := []string{"field1", "field2", "field3"}
|
||||
sorts := []int32{1, -1, 1}
|
||||
db.PopulateMultiIndex(ctx, keys, sorts, false)
|
||||
}
|
||||
|
||||
func TestDocumentDB_GetClient(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
client := db.GetClient()
|
||||
if client == nil {
|
||||
t.Error("Expected client to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDB_DatabaseName(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test that we got a valid DocumentDB
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDB to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDB_WithDifferentConfigs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf *Conf
|
||||
}{
|
||||
{
|
||||
name: "minimal config",
|
||||
conf: &Conf{
|
||||
Host: "localhost:27017",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with database",
|
||||
conf: &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with credentials",
|
||||
conf: &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
db, err := NewDocumentDB(tc.conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDB to be non-nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDB_IndexOperations(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test single index
|
||||
ctx := context.Background()
|
||||
db.PopulateIndex(ctx, "single_field", 1, false)
|
||||
|
||||
// Test TTL index
|
||||
ttl := int32(1800) // 30 minutes
|
||||
db.PopulateTTLIndex(ctx, "expiresAt", 1, false, ttl)
|
||||
|
||||
// Test multiple indexes
|
||||
keys := []string{"field1", "field2", "compound_field1"}
|
||||
sorts := []int32{1, -1, 1}
|
||||
db.PopulateMultiIndex(ctx, keys, sorts, false)
|
||||
}
|
||||
|
||||
func TestDocumentDB_ContextHandling(t *testing.T) {
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
// Test with timeout context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
// Use ctx to avoid unused variable warning
|
||||
_ = ctx
|
||||
|
||||
if err != nil {
|
||||
t.Logf("MongoDB connection failed (expected in test environment): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
t.Error("Expected DocumentDB to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDB_ErrorHandling(t *testing.T) {
|
||||
// Test with invalid config
|
||||
invalidConf := &Conf{
|
||||
Host: "invalid-host:99999",
|
||||
}
|
||||
|
||||
_, err := NewDocumentDB(invalidConf, "testcollection")
|
||||
|
||||
// This should fail
|
||||
if err == nil {
|
||||
t.Error("Expected error with invalid host, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentDB_IndexModelCreation(t *testing.T) {
|
||||
// Test the yieldIndexModel function indirectly through PopulateIndex
|
||||
conf := &Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
}
|
||||
|
||||
db, err := NewDocumentDB(conf, "testcollection")
|
||||
|
||||
if err != nil {
|
||||
t.Skip("Skipping test - MongoDB not available")
|
||||
return
|
||||
}
|
||||
|
||||
// Test with various index configurations
|
||||
ctx := context.Background()
|
||||
db.PopulateIndex(ctx, "ascending", 1, false)
|
||||
db.PopulateIndex(ctx, "descending", -1, false)
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type TypeCodec struct {
|
||||
ValueType reflect.Type
|
||||
Encoder bson.ValueEncoder
|
||||
Decoder bson.ValueDecoder
|
||||
}
|
||||
|
||||
// WithTypeCodec registers TypeCodecs to convert custom types.
|
||||
func WithTypeCodec(typeCodecs ...TypeCodec) mon.Option {
|
||||
return func(c *options.ClientOptions) {
|
||||
registry := bson.NewRegistry()
|
||||
for _, v := range typeCodecs {
|
||||
registry.RegisterTypeEncoder(v.ValueType, v.Encoder)
|
||||
registry.RegisterTypeDecoder(v.ValueType, v.Decoder)
|
||||
}
|
||||
c.SetRegistry(registry)
|
||||
}
|
||||
}
|
||||
|
||||
// SetCustomDecimalType force convert primitive.Decimal128 to decimal.Decimal.
|
||||
func SetCustomDecimalType() mon.Option {
|
||||
return WithTypeCodec(TypeCodec{
|
||||
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||
Encoder: &MgoDecimal{},
|
||||
Decoder: &MgoDecimal{},
|
||||
})
|
||||
}
|
||||
|
||||
func InitMongoOptions(cfg Conf) mon.Option {
|
||||
return func(opts *options.ClientOptions) {
|
||||
opts.SetMaxPoolSize(cfg.MaxPoolSize)
|
||||
opts.SetMinPoolSize(cfg.MinPoolSize)
|
||||
opts.SetMaxConnIdleTime(cfg.MaxConnIdleTime)
|
||||
opts.SetCompressors([]string{"snappy"})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
)
|
||||
|
||||
func TestWithTypeCodec(t *testing.T) {
|
||||
// Test creating a TypeCodec
|
||||
codec := TypeCodec{
|
||||
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||
Encoder: &MgoDecimal{},
|
||||
Decoder: &MgoDecimal{},
|
||||
}
|
||||
|
||||
if codec.ValueType != reflect.TypeOf(decimal.Decimal{}) {
|
||||
t.Errorf("Expected ValueType to be decimal.Decimal, got %v", codec.ValueType)
|
||||
}
|
||||
|
||||
if codec.Encoder == nil {
|
||||
t.Error("Expected Encoder to be set")
|
||||
}
|
||||
|
||||
if codec.Decoder == nil {
|
||||
t.Error("Expected Decoder to be set")
|
||||
}
|
||||
|
||||
// Test WithTypeCodec function
|
||||
option := WithTypeCodec(codec)
|
||||
if option == nil {
|
||||
t.Error("Expected option to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCustomDecimalType(t *testing.T) {
|
||||
// Test setting custom decimal type
|
||||
option := SetCustomDecimalType()
|
||||
|
||||
// Verify that the option is created
|
||||
if option == nil {
|
||||
t.Error("Expected option to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitMongoOptions(t *testing.T) {
|
||||
// Test with default config
|
||||
conf := Conf{}
|
||||
opts := InitMongoOptions(conf)
|
||||
|
||||
if opts == nil {
|
||||
t.Error("Expected options to be non-nil")
|
||||
}
|
||||
|
||||
// Test with custom config
|
||||
confWithValues := Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
User: "testuser",
|
||||
Password: "testpass",
|
||||
}
|
||||
optsWithValues := InitMongoOptions(confWithValues)
|
||||
|
||||
if optsWithValues == nil {
|
||||
t.Error("Expected options to be non-nil")
|
||||
}
|
||||
|
||||
// Test that the options are properly configured
|
||||
// We can't directly test the internal configuration, but we can test that it doesn't panic
|
||||
}
|
||||
|
||||
func TestTypeCodec_InterfaceCompliance(t *testing.T) {
|
||||
codec := TypeCodec{
|
||||
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||
Encoder: &MgoDecimal{},
|
||||
Decoder: &MgoDecimal{},
|
||||
}
|
||||
|
||||
// Test that the codec can be used
|
||||
if codec.ValueType == nil {
|
||||
t.Error("Expected ValueType to be set")
|
||||
}
|
||||
|
||||
if codec.Encoder == nil {
|
||||
t.Error("Expected Encoder to be set")
|
||||
}
|
||||
|
||||
if codec.Decoder == nil {
|
||||
t.Error("Expected Decoder to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMgoDecimal_WithRegistry(t *testing.T) {
|
||||
// Test that MgoDecimal can be used with a registry
|
||||
option := SetCustomDecimalType()
|
||||
|
||||
// Test that the option is created
|
||||
if option == nil {
|
||||
t.Error("Expected option to be non-nil")
|
||||
}
|
||||
|
||||
// Test basic decimal operations
|
||||
dec := decimal.NewFromFloat(123.45)
|
||||
|
||||
// Test that decimal operations work
|
||||
if dec.IsZero() {
|
||||
t.Error("Expected decimal to be non-zero")
|
||||
}
|
||||
|
||||
// Test string conversion
|
||||
decStr := dec.String()
|
||||
if decStr != "123.45" {
|
||||
t.Errorf("Expected '123.45', got '%s'", decStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitMongoOptions_WithDifferentConfigs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
conf Conf
|
||||
}{
|
||||
{
|
||||
name: "empty config",
|
||||
conf: Conf{},
|
||||
},
|
||||
{
|
||||
name: "with host",
|
||||
conf: Conf{
|
||||
Host: "localhost:27017",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with database",
|
||||
conf: Conf{
|
||||
Database: "testdb",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with credentials",
|
||||
conf: Conf{
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "full config",
|
||||
conf: Conf{
|
||||
Host: "localhost:27017",
|
||||
Database: "testdb",
|
||||
User: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := InitMongoOptions(tc.conf)
|
||||
if opts == nil {
|
||||
t.Error("Expected options to be non-nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTypeCodec_EdgeCases(t *testing.T) {
|
||||
// Test with nil encoder
|
||||
codec := TypeCodec{
|
||||
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||
Encoder: nil,
|
||||
Decoder: &MgoDecimal{},
|
||||
}
|
||||
|
||||
if codec.Encoder != nil {
|
||||
t.Error("Expected Encoder to be nil")
|
||||
}
|
||||
|
||||
if codec.Decoder == nil {
|
||||
t.Error("Expected Decoder to be set")
|
||||
}
|
||||
|
||||
// Test with nil decoder
|
||||
codec2 := TypeCodec{
|
||||
ValueType: reflect.TypeOf(decimal.Decimal{}),
|
||||
Encoder: &MgoDecimal{},
|
||||
Decoder: nil,
|
||||
}
|
||||
|
||||
if codec2.Encoder == nil {
|
||||
t.Error("Expected Encoder to be set")
|
||||
}
|
||||
|
||||
if codec2.Decoder != nil {
|
||||
t.Error("Expected Decoder to be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCustomDecimalType_MultipleCalls(t *testing.T) {
|
||||
// Test calling SetCustomDecimalType multiple times
|
||||
|
||||
// First call
|
||||
option1 := SetCustomDecimalType()
|
||||
|
||||
// Second call should not panic
|
||||
option2 := SetCustomDecimalType()
|
||||
|
||||
// Options should be valid
|
||||
if option1 == nil {
|
||||
t.Error("Expected option1 to be non-nil")
|
||||
}
|
||||
|
||||
if option2 == nil {
|
||||
t.Error("Expected option2 to be non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitMongoOptions_ReturnType(t *testing.T) {
|
||||
conf := Conf{}
|
||||
opts := InitMongoOptions(conf)
|
||||
|
||||
// Test that the returned type is correct
|
||||
if opts == nil {
|
||||
t.Error("Expected options to be non-nil")
|
||||
}
|
||||
|
||||
// Test that we can use the options (basic type check)
|
||||
var _ mon.Option = opts
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
mopt "go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type DocumentDBUseCase interface {
|
||||
PopulateIndex(ctx context.Context, key string, sort int32, unique bool)
|
||||
PopulateTTLIndex(ctx context.Context, key string, sort int32, unique bool, ttl int32)
|
||||
PopulateMultiIndex(ctx context.Context, keys []string, sorts []int32, unique bool)
|
||||
GetClient() *mon.Model
|
||||
}
|
||||
|
||||
type DocumentDBWithCacheUseCase interface {
|
||||
DocumentDBUseCase
|
||||
CacheUseCase
|
||||
DeleteOne(ctx context.Context, key string, filter any, opts ...*mopt.DeleteOneOptions) (int64, error)
|
||||
FindOne(ctx context.Context, key string, v, filter any, opts ...*mopt.FindOneOptions) error
|
||||
FindOneAndDelete(ctx context.Context, key string, v, filter any, opts ...*mopt.FindOneAndDeleteOptions) error
|
||||
FindOneAndReplace(ctx context.Context, key string, v, filter, replacement any, opts ...*mopt.FindOneAndReplaceOptions) error
|
||||
InsertOne(ctx context.Context, key string, document any, opts ...*mopt.InsertOneOptions) (*mongo.InsertOneResult, error)
|
||||
UpdateByID(ctx context.Context, key string, id, update any, opts ...*mopt.UpdateOneOptions) (*mongo.UpdateResult, error)
|
||||
UpdateMany(ctx context.Context, keys []string, filter, update any, opts ...*mopt.UpdateManyOptions) (*mongo.UpdateResult, error)
|
||||
UpdateOne(ctx context.Context, key string, filter, update any, opts ...*mopt.UpdateOneOptions) (*mongo.UpdateResult, error)
|
||||
}
|
||||
|
||||
type CacheUseCase interface {
|
||||
DelCache(ctx context.Context, keys ...string) error
|
||||
GetCache(key string, v any) error
|
||||
SetCache(key string, v any) error
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package config
|
||||
|
||||
type Config struct {
|
||||
// 密碼加密層數
|
||||
Bcrypt struct {
|
||||
Cost int
|
||||
}
|
||||
|
||||
GoogleAuth struct {
|
||||
ClientID string
|
||||
AuthURL string
|
||||
}
|
||||
|
||||
LineAuth struct {
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
RedirectURI string
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
// Package domain defines the core domain entities, constants, and business rules
|
||||
// for the member service. It contains the fundamental building blocks that
|
||||
// represent the business concepts and their relationships.
|
||||
package domain
|
||||
|
||||
// Business constants for the member service
|
||||
const (
|
||||
// DefaultBcryptCost is the default cost for bcrypt password hashing
|
||||
DefaultBcryptCost = 10
|
||||
|
||||
// MinPasswordLength is the minimum required password length
|
||||
MinPasswordLength = 8
|
||||
|
||||
// MaxPasswordLength is the maximum allowed password length
|
||||
MaxPasswordLength = 128
|
||||
|
||||
// DefaultVerifyCodeDigits is the default number of digits for verification codes
|
||||
DefaultVerifyCodeDigits = 6
|
||||
|
||||
// MinVerifyCodeDigits is the minimum number of digits for verification codes
|
||||
MinVerifyCodeDigits = 4
|
||||
|
||||
// MaxVerifyCodeDigits is the maximum number of digits for verification codes
|
||||
MaxVerifyCodeDigits = 10
|
||||
|
||||
// DefaultCacheExpiration is the default cache expiration time in seconds
|
||||
DefaultCacheExpiration = 3600
|
||||
|
||||
// MaxRetryAttempts is the maximum number of retry attempts for operations
|
||||
MaxRetryAttempts = 3
|
||||
)
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/member"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// Account represents a user account with authentication credentials.
|
||||
// It stores login information, hashed passwords, and platform-specific data.
|
||||
type Account struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
LoginID string `bson:"login_id"` // Unique login identifier (email, phone, username)
|
||||
Token string `bson:"token"` // Hashed password or platform-specific token
|
||||
Platform member.Platform `bson:"platform"` // Platform type: 1. platform 2. google 3. line 4. apple
|
||||
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
|
||||
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
|
||||
}
|
||||
|
||||
// CollectionName returns the MongoDB collection name for Account entities.
|
||||
func (a *Account) CollectionName() string {
|
||||
return "account"
|
||||
}
|
||||
|
||||
// Validate validates the Account entity
|
||||
func (a *Account) Validate() error {
|
||||
if a.LoginID == "" {
|
||||
return errors.New("login_id is required")
|
||||
}
|
||||
if a.Token == "" {
|
||||
return errors.New("token is required")
|
||||
}
|
||||
if !a.Platform.IsValid() {
|
||||
return errors.New("invalid platform")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimestamps sets the create and update timestamps
|
||||
func (a *Account) SetTimestamps() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
if a.CreateAt == nil {
|
||||
a.CreateAt = &now
|
||||
}
|
||||
a.UpdateAt = &now
|
||||
}
|
||||
|
||||
// IsNew returns true if this is a new account (no ID set)
|
||||
func (a *Account) IsNew() bool {
|
||||
return a.ID.IsZero()
|
||||
}
|
||||
|
||||
// GetIDString returns the ID as a hex string
|
||||
func (a *Account) GetIDString() string {
|
||||
return a.ID.Hex()
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/member"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type AccountUID struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
LoginID string `bson:"login_id"`
|
||||
UID string `bson:"uid"`
|
||||
Type member.AccountType `bson:"type"`
|
||||
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
|
||||
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AccountUID) CollectionName() string {
|
||||
return "account_uid_binding"
|
||||
}
|
||||
|
||||
// Validate validates the AccountUID entity
|
||||
func (a *AccountUID) Validate() error {
|
||||
if a.LoginID == "" {
|
||||
return errors.New("login_id is required")
|
||||
}
|
||||
if a.UID == "" {
|
||||
return errors.New("uid is required")
|
||||
}
|
||||
if !a.Type.IsValid() {
|
||||
return errors.New("invalid account type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimestamps sets the create and update timestamps
|
||||
func (a *AccountUID) SetTimestamps() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
if a.CreateAt == nil {
|
||||
a.CreateAt = &now
|
||||
}
|
||||
a.UpdateAt = &now
|
||||
}
|
||||
|
||||
// IsNew returns true if this is a new binding (no ID set)
|
||||
func (a *AccountUID) IsNew() bool {
|
||||
return a.ID.IsZero()
|
||||
}
|
||||
|
||||
// GetIDString returns the ID as a hex string
|
||||
func (a *AccountUID) GetIDString() string {
|
||||
return a.ID.Hex()
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
type AutoID struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
Name string `bson:"name,omitempty" json:"name,omitempty"`
|
||||
Counter uint64 `bson:"counter" json:"counter"`
|
||||
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
|
||||
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
|
||||
}
|
||||
|
||||
func (a *AutoID) CollectionName() string {
|
||||
return "count"
|
||||
}
|
||||
|
||||
// Validate validates the AutoID entity
|
||||
func (a *AutoID) Validate() error {
|
||||
if a.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimestamps sets the create and update timestamps
|
||||
func (a *AutoID) SetTimestamps() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
if a.CreateAt == nil {
|
||||
a.CreateAt = &now
|
||||
}
|
||||
a.UpdateAt = &now
|
||||
}
|
||||
|
||||
// IsNew returns true if this is a new counter (no ID set)
|
||||
func (a *AutoID) IsNew() bool {
|
||||
return a.ID.IsZero()
|
||||
}
|
||||
|
||||
// GetIDString returns the ID as a hex string
|
||||
func (a *AutoID) GetIDString() string {
|
||||
return a.ID.Hex()
|
||||
}
|
||||
|
||||
// Increment increments the counter by 1
|
||||
func (a *AutoID) Increment() {
|
||||
a.Counter++
|
||||
a.SetTimestamps()
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/member"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
)
|
||||
|
||||
// User represents a user profile with personal information and preferences.
|
||||
// It contains detailed user data that is separate from authentication credentials.
|
||||
type User struct {
|
||||
ID bson.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
|
||||
UID string `bson:"uid"` // Unique user identifier
|
||||
AvatarURL *string `bson:"avatar_url,omitempty"` // User avatar URL (optional)
|
||||
FullName *string `bson:"full_name,omitempty"` // User's full name
|
||||
Nickname *string `bson:"nickname,omitempty"` // User's nickname (optional)
|
||||
GenderCode *int64 `bson:"gender_code,omitempty"` // Gender code
|
||||
Birthdate *int64 `bson:"birthdate,omitempty"` // Birth date (format: 19930417)
|
||||
Address *string `bson:"address,omitempty"` // User's address
|
||||
AlarmCategory member.AlarmType `bson:"alarm_category"` // Alert notification settings
|
||||
UserStatus member.Status `bson:"user_status"` // User account status
|
||||
PreferredLanguage string `bson:"preferred_language"` // User's preferred language
|
||||
Currency string `bson:"currency"` // User's preferred currency
|
||||
PhoneNumber *string `bson:"phone_number,omitempty"` // Phone number (appears after verification)
|
||||
Email *string `bson:"email,omitempty"` // Email address (appears after verification)
|
||||
UpdateAt *int64 `bson:"update_at,omitempty" json:"update_at,omitempty"`
|
||||
CreateAt *int64 `bson:"create_at,omitempty" json:"create_at,omitempty"`
|
||||
}
|
||||
|
||||
// CollectionName returns the MongoDB collection name for User entities.
|
||||
func (u *User) CollectionName() string {
|
||||
return "user_info"
|
||||
}
|
||||
|
||||
// Validate validates the User entity
|
||||
func (u *User) Validate() error {
|
||||
if u.UID == "" {
|
||||
return errors.New("uid is required")
|
||||
}
|
||||
if u.PreferredLanguage == "" {
|
||||
return errors.New("preferred_language is required")
|
||||
}
|
||||
if u.Currency == "" {
|
||||
return errors.New("currency is required")
|
||||
}
|
||||
if !u.AlarmCategory.IsValid() {
|
||||
return errors.New("invalid alarm_category")
|
||||
}
|
||||
if !u.UserStatus.IsValid() {
|
||||
return errors.New("invalid user_status")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimestamps sets the create and update timestamps
|
||||
func (u *User) SetTimestamps() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
if u.CreateAt == nil {
|
||||
u.CreateAt = &now
|
||||
}
|
||||
u.UpdateAt = &now
|
||||
}
|
||||
|
||||
// IsNew returns true if this is a new user (no ID set)
|
||||
func (u *User) IsNew() bool {
|
||||
return u.ID.IsZero()
|
||||
}
|
||||
|
||||
// GetIDString returns the ID as a hex string
|
||||
func (u *User) GetIDString() string {
|
||||
return u.ID.Hex()
|
||||
}
|
||||
|
||||
// GetDisplayName returns the display name (nickname or full name)
|
||||
func (u *User) GetDisplayName() string {
|
||||
if u.Nickname != nil && *u.Nickname != "" {
|
||||
return *u.Nickname
|
||||
}
|
||||
if u.FullName != nil && *u.FullName != "" {
|
||||
return *u.FullName
|
||||
}
|
||||
return u.UID
|
||||
}
|
||||
|
||||
// IsActive returns true if the user status is active
|
||||
func (u *User) IsActive() bool {
|
||||
return u.UserStatus == member.AccountStatusActive
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package domain
|
||||
|
||||
import "code.30cm.net/digimon/library-go/errs"
|
||||
|
||||
// Verify Error Code
|
||||
const (
|
||||
FailedToVerifyGoogle errs.ErrorCode = iota + 1
|
||||
FailedToVerifyGoogleTimeout
|
||||
FailedToVerifyGoogleHTTPCode
|
||||
FailedToVerifyGoogleTokenExpired
|
||||
FailedToVerifyGoogleInvalidAudience
|
||||
FailedToVerifyLine
|
||||
)
|
||||
|
||||
// PWS Error Code
|
||||
const (
|
||||
HashPasswordErrorCode = 10 + iota
|
||||
InsertAccountErrorCode
|
||||
BindingUserTabletErrorCode
|
||||
FailedToFindAccountErrorCode
|
||||
FailedToBindAccountErrorCode
|
||||
FailedToIncAccountErrorCode
|
||||
FailedFindUIDByLoginIDErrorCode
|
||||
FailedFindOneByAccountErrorCode
|
||||
FailedToUpdatePasswordErrorCode
|
||||
FailedToFindUserErrorCode
|
||||
FailedToUpdateUserErrorCode
|
||||
FailedToUpdateUserStatusErrorCode
|
||||
FailedToGetUserInfoErrorCode
|
||||
FailedToGetVerifyCodeErrorCode
|
||||
FailedToGetCodeOnRedisErrorCode
|
||||
FailedToGetCodeCorrectErrorCode
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package member
|
||||
|
||||
type AccountType int32
|
||||
|
||||
const (
|
||||
AccountTypeNone AccountType = -1
|
||||
AccountTypePhone AccountType = 1 // 手機
|
||||
AccountTypeMail AccountType = 2 // 信箱
|
||||
AccountTypeDefine AccountType = 3 // 自定義帳號
|
||||
)
|
||||
|
||||
var convAccountTypeCode = map[string]AccountType{
|
||||
"phone": AccountTypePhone,
|
||||
"email": AccountTypeMail,
|
||||
"platform": AccountTypeDefine,
|
||||
}
|
||||
|
||||
func GetAccountTypeByCode(code string) AccountType {
|
||||
result, ok := convAccountTypeCode[code]
|
||||
if !ok {
|
||||
return AccountTypeNone
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// IsValid returns true if the account type is valid
|
||||
func (a AccountType) IsValid() bool {
|
||||
return a >= AccountTypePhone && a <= AccountTypeDefine
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package member
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetAccountTypeByCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
expected AccountType
|
||||
}{
|
||||
{"phone", AccountTypePhone}, // 測試有效值: 手機
|
||||
{"email", AccountTypeMail}, // 測試有效值: 信箱
|
||||
{"platform", AccountTypeDefine}, // 測試有效值: 自定義帳號
|
||||
{"unknown", AccountTypeNone}, // 測試無效值
|
||||
{"", AccountTypeNone}, // 測試空字串
|
||||
{"PHONE", AccountTypeNone}, // 測試大小寫不匹配
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.code, func(t *testing.T) {
|
||||
result := GetAccountTypeByCode(tt.code)
|
||||
if result != tt.expected {
|
||||
t.Errorf("GetAccountTypeByCode(%v) = %v, want %v", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package member
|
||||
|
||||
// AlarmType 警報類型
|
||||
type AlarmType int32
|
||||
|
||||
func (a *AlarmType) CodeToString() string {
|
||||
result, ok := verifyAlarmMap[*a]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var verifyAlarmMap = map[AlarmType]string{
|
||||
AlarmUninitialized: "uninitialized", // 初始狀態(異常)
|
||||
AlarmNoAlert: "no_alert", // 未告警
|
||||
AlarmSystem: "system_alert", // 系統告警中
|
||||
}
|
||||
|
||||
const (
|
||||
AlarmUninitialized AlarmType = 0 // 初始狀態(異常)
|
||||
AlarmNoAlert AlarmType = 1 // 未告警
|
||||
AlarmSystem AlarmType = 2 // 系統告警中
|
||||
)
|
||||
|
||||
// IsValid returns true if the alarm type is valid
|
||||
func (a AlarmType) IsValid() bool {
|
||||
return a >= AlarmUninitialized && a <= AlarmSystem
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package member
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAlarmType_CodeToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
alarmType AlarmType
|
||||
expected string
|
||||
}{
|
||||
{AlarmUninitialized, "uninitialized"}, // 測試初始狀態
|
||||
{AlarmNoAlert, "no_alert"}, // 測試未告警狀態
|
||||
{AlarmSystem, "system_alert"}, // 測試系統告警
|
||||
{AlarmType(999), ""}, // 測試不存在的狀態
|
||||
{AlarmType(-1), ""}, // 測試無效負數狀態
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
result := tt.alarmType.CodeToString()
|
||||
if result != tt.expected {
|
||||
t.Errorf("CodeToString() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package member
|
||||
|
||||
type GenerateCodeType int64
|
||||
|
||||
const (
|
||||
GenerateCodeTypeNone GenerateCodeType = -1
|
||||
GenerateCodeTypeEmail GenerateCodeType = 1 // email 驗證碼
|
||||
GenerateCodeTypePhone GenerateCodeType = 2 // phone 驗證碼
|
||||
GenerateCodeTypeForgetPassword GenerateCodeType = 3 // 忘記密碼
|
||||
)
|
||||
|
||||
var codeMap = map[GenerateCodeType]string{
|
||||
1: "email",
|
||||
2: "phone",
|
||||
3: "forget_email",
|
||||
}
|
||||
|
||||
func GetCodeNameByCode(code GenerateCodeType) (string, bool) {
|
||||
res, ok := codeMap[code]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return res, true
|
||||
}
|
||||
|
||||
var generateCodeTypeMap = map[string]GenerateCodeType{
|
||||
"email": GenerateCodeTypeEmail,
|
||||
"phone": GenerateCodeTypePhone,
|
||||
"forget_email": GenerateCodeTypeForgetPassword,
|
||||
}
|
||||
|
||||
func GetGetCodeNameByCode(code string) GenerateCodeType {
|
||||
result, ok := generateCodeTypeMap[code]
|
||||
if !ok {
|
||||
return GenerateCodeTypeNone
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package member
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetCodeNameByCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
code GenerateCodeType
|
||||
expected string
|
||||
ok bool
|
||||
}{
|
||||
{GenerateCodeTypeEmail, "email", true},
|
||||
{GenerateCodeTypePhone, "phone", true},
|
||||
{GenerateCodeTypeForgetPassword, "forget_email", true},
|
||||
{GenerateCodeTypeNone, "", false}, // 無效代碼
|
||||
{GenerateCodeType(999), "", false}, // 無效代碼
|
||||
{GenerateCodeType(-999), "", false}, // 無效代碼
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
result, ok := GetCodeNameByCode(tt.code)
|
||||
if result != tt.expected || ok != tt.ok {
|
||||
t.Errorf("GetCodeNameByCode(%v) = (%v, %v), want (%v, %v)",
|
||||
tt.code, result, ok, tt.expected, tt.ok)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetGetCodeNameByCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expected GenerateCodeType
|
||||
}{
|
||||
{"email", GenerateCodeTypeEmail},
|
||||
{"phone", GenerateCodeTypePhone},
|
||||
{"forget_email", GenerateCodeTypeForgetPassword},
|
||||
{"unknown", GenerateCodeTypeNone}, // 無效名稱
|
||||
{"", GenerateCodeTypeNone}, // 空名稱
|
||||
{"123", GenerateCodeTypeNone}, // 無效名稱
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := GetGetCodeNameByCode(tt.name)
|
||||
if result != tt.expected {
|
||||
t.Errorf("GetGetCodeNameByCode(%v) = %v, want %v",
|
||||
tt.name, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package member
|
||||
|
||||
const (
|
||||
Digimon Platform = 1 + iota
|
||||
Google
|
||||
Line
|
||||
Apple
|
||||
)
|
||||
|
||||
const (
|
||||
PlatformNone Platform = -1
|
||||
)
|
||||
|
||||
const (
|
||||
DigimonString = "platform"
|
||||
GoogleString = "google"
|
||||
LineString = "line"
|
||||
AppleString = "apple"
|
||||
)
|
||||
|
||||
type Platform int8
|
||||
|
||||
func (p Platform) ToInt64() int64 {
|
||||
return int64(p)
|
||||
}
|
||||
|
||||
// ToString - 將 Platform 轉為文字
|
||||
func (p Platform) ToString() string {
|
||||
if result, ok := platformToString[p]; ok {
|
||||
return result
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
var platformToString = map[Platform]string{
|
||||
Digimon: DigimonString,
|
||||
Google: GoogleString,
|
||||
Line: LineString,
|
||||
Apple: AppleString,
|
||||
}
|
||||
|
||||
var stringToPlatform = map[string]Platform{
|
||||
DigimonString: Digimon,
|
||||
GoogleString: Google,
|
||||
LineString: Line,
|
||||
AppleString: Apple,
|
||||
}
|
||||
|
||||
// GetPlatformByPlatformCode - 從文字轉為 Platform
|
||||
func GetPlatformByPlatformCode(code string) Platform {
|
||||
if result, ok := stringToPlatform[code]; ok {
|
||||
return result
|
||||
}
|
||||
|
||||
return PlatformNone
|
||||
}
|
||||
|
||||
// IsValid returns true if the platform is valid
|
||||
func (p Platform) IsValid() bool {
|
||||
return p >= Digimon && p <= Apple
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package member
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPlatform_ToInt64(t *testing.T) {
|
||||
tests := []struct {
|
||||
platform Platform
|
||||
expected int64
|
||||
}{
|
||||
{Digimon, 1},
|
||||
{Google, 2},
|
||||
{Line, 3},
|
||||
{Apple, 4},
|
||||
{PlatformNone, -1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.platform.ToString(), func(t *testing.T) {
|
||||
if result := tt.platform.ToInt64(); result != tt.expected {
|
||||
t.Errorf("ToInt64() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatform_ToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
platform Platform
|
||||
expected string
|
||||
}{
|
||||
{Digimon, DigimonString},
|
||||
{Google, GoogleString},
|
||||
{Line, LineString},
|
||||
{Apple, AppleString},
|
||||
{PlatformNone, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
if result := tt.platform.ToString(); result != tt.expected {
|
||||
t.Errorf("ToString() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPlatformByPlatformCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
code string
|
||||
expected Platform
|
||||
}{
|
||||
{DigimonString, Digimon},
|
||||
{GoogleString, Google},
|
||||
{LineString, Line},
|
||||
{AppleString, Apple},
|
||||
{"unknown", PlatformNone}, // 測試不存在的 Platform
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.code, func(t *testing.T) {
|
||||
if result := GetPlatformByPlatformCode(tt.code); result != tt.expected {
|
||||
t.Errorf("GetPlatformByPlatformCode(%v) = %v, want %v", tt.code, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package member
|
||||
|
||||
// Status 會員狀態
|
||||
type Status int32
|
||||
|
||||
func (s *Status) CodeToString() string {
|
||||
result, ok := accountStatusMap[*s]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var accountStatusMap = map[Status]string{
|
||||
AccountStatusUninitialized: "uninitialized", // 初始狀態(異常)
|
||||
AccountStatusUnverified: "unverified", // 尚未完成驗證
|
||||
AccountStatusActive: "active", // 帳號啟用中
|
||||
AccountStatusSuspended: "suspended", // 帳號停權中
|
||||
}
|
||||
|
||||
func (s *Status) ToInt32() int32 {
|
||||
return int32(*s)
|
||||
}
|
||||
|
||||
const (
|
||||
AccountStatusUninitialized Status = 0 // 初始狀態(異常)
|
||||
AccountStatusUnverified Status = 1 // 尚未驗證
|
||||
AccountStatusActive Status = 2 // 帳號啟用中
|
||||
AccountStatusSuspended Status = 3 // 帳號停權中
|
||||
)
|
||||
|
||||
// IsValid returns true if the status is valid
|
||||
func (s Status) IsValid() bool {
|
||||
return s >= AccountStatusUninitialized && s <= AccountStatusSuspended
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package member
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStatus_CodeToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
status Status
|
||||
expected string
|
||||
}{
|
||||
{AccountStatusUninitialized, "uninitialized"},
|
||||
{AccountStatusUnverified, "unverified"},
|
||||
{AccountStatusActive, "active"},
|
||||
{AccountStatusSuspended, "suspended"},
|
||||
{Status(999), ""}, // 測試不存在的狀態
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
if result := tt.status.CodeToString(); result != tt.expected {
|
||||
t.Errorf("CodeToString() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus_ToInt32(t *testing.T) {
|
||||
tests := []struct {
|
||||
status Status
|
||||
expected int32
|
||||
}{
|
||||
{AccountStatusUninitialized, 0},
|
||||
{AccountStatusUnverified, 1},
|
||||
{AccountStatusActive, 2},
|
||||
{AccountStatusSuspended, 3},
|
||||
{Status(999), 999}, // 測試不存在的狀態
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(string(tt.expected), func(t *testing.T) {
|
||||
if result := tt.status.ToInt32(); result != tt.expected {
|
||||
t.Errorf("ToInt32() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RedisKey represents a Redis key type with helper methods for key construction.
|
||||
type RedisKey string
|
||||
|
||||
const (
|
||||
// AccountRedisKey is the Redis key prefix for account data
|
||||
AccountRedisKey RedisKey = "account"
|
||||
// AccountUIDRedisKey is the Redis key prefix for account-UID binding data
|
||||
AccountUIDRedisKey RedisKey = "account_uid"
|
||||
// UserRedisKey is the Redis key prefix for user data
|
||||
UserRedisKey RedisKey = "user"
|
||||
// MemberPrefixRedisKey is the common prefix for all member-related Redis keys
|
||||
MemberPrefixRedisKey = "member"
|
||||
)
|
||||
|
||||
// ToString converts the RedisKey to its full string representation with the member prefix.
|
||||
func (key RedisKey) ToString() string {
|
||||
return "member:" + string(key)
|
||||
}
|
||||
|
||||
// With appends additional parts to the RedisKey, separated by colons.
|
||||
func (key RedisKey) With(s ...string) RedisKey {
|
||||
parts := append([]string{string(key)}, s...)
|
||||
return RedisKey(strings.Join(parts, ":"))
|
||||
}
|
||||
|
||||
// GetAccountRedisKey generates a Redis key for account data by ID.
|
||||
func GetAccountRedisKey(id string) string {
|
||||
return AccountRedisKey.With(id).ToString()
|
||||
}
|
||||
|
||||
// GetAccountUIDRedisKey generates a Redis key for account-UID binding data by ID.
|
||||
func GetAccountUIDRedisKey(id string) string {
|
||||
return AccountUIDRedisKey.With(id).ToString()
|
||||
}
|
||||
|
||||
// GetUserRedisKey generates a Redis key for user data by ID.
|
||||
func GetUserRedisKey(id string) string {
|
||||
return UserRedisKey.With(id).ToString()
|
||||
}
|
||||
|
||||
var (
|
||||
// checkVerifyKey is the Redis key prefix for verification codes
|
||||
checkVerifyKey = fmt.Sprintf("%s:verify:", MemberPrefixRedisKey)
|
||||
)
|
||||
|
||||
// GetCheckVerifyKey generates a Redis key for verification code data.
|
||||
func GetCheckVerifyKey(codeType, account string) string {
|
||||
return fmt.Sprintf("%s%s:%s", checkVerifyKey, codeType, account)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/pkg/member/domain/member"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type AccountRepository interface {
|
||||
Insert(ctx context.Context, data *entity.Account) error
|
||||
FindOne(ctx context.Context, id string) (*entity.Account, error)
|
||||
Update(ctx context.Context, data *entity.Account) (*mongo.UpdateResult, error)
|
||||
Delete(ctx context.Context, id string) (int64, error)
|
||||
FindOneByAccount(ctx context.Context, loginID string) (*entity.Account, error)
|
||||
UpdateTokenByLoginID(ctx context.Context, account string, token string, platform member.Platform) error
|
||||
AccountIndexUP
|
||||
}
|
||||
|
||||
type AccountIndexUP interface {
|
||||
Index20241226001UP(ctx context.Context) (*mongo.Cursor, error)
|
||||
}
|
||||
|
||||
// type AccountIndexDown interface {
|
||||
// Index20241226001Down(ctx context.Context)
|
||||
// }
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type AccountUIDRepository interface {
|
||||
Insert(ctx context.Context, data *entity.AccountUID) error
|
||||
FindOne(ctx context.Context, id string) (*entity.AccountUID, error)
|
||||
Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error)
|
||||
Delete(ctx context.Context, id string) (int64, error)
|
||||
FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error)
|
||||
AccountUIDIndexUP
|
||||
}
|
||||
|
||||
// Index20241226001Up 這樣方便管理,知道是 這張表 20241226 第一個新增的 index 之後有版本遷移也可以這樣寫,先快速這樣做,等之後找到更好的迭代工具,在用
|
||||
|
||||
type AccountUIDIndexUP interface {
|
||||
Index20241226001UP(ctx context.Context) (*mongo.Cursor, error)
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type AutoIDRepository interface {
|
||||
Insert(ctx context.Context, data *entity.AutoID) error
|
||||
FindOne(ctx context.Context, id string) (*entity.AutoID, error)
|
||||
Update(ctx context.Context, data *entity.AutoID) (*mongo.UpdateResult, error)
|
||||
Delete(ctx context.Context, id string) (int64, error)
|
||||
Inc(ctx context.Context, data *entity.AutoID) error
|
||||
GetUIDFromNum(num int64) (string, error)
|
||||
GetNumFromUID(uid string) (int64, error)
|
||||
AutoIDIndexUP
|
||||
}
|
||||
|
||||
type AutoIDIndexUP interface {
|
||||
Index20241226001UP(ctx context.Context) (*mongo.Cursor, error)
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/member"
|
||||
|
||||
"go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
BaseUserRepository
|
||||
UpdateUserDetailsByUID(ctx context.Context, data *UpdateUserInfoRequest) error
|
||||
UpdateStatus(ctx context.Context, uid string, status int32) error
|
||||
FindOneByUID(ctx context.Context, uid string) (*entity.User, error)
|
||||
FindOneByNickName(ctx context.Context, nickName string) (*entity.User, error)
|
||||
ListMembers(ctx context.Context, params *UserQueryParams) ([]*entity.User, int64, error)
|
||||
UpdateEmailVerifyStatus(ctx context.Context, uid, email string) error
|
||||
UpdatePhoneVerifyStatus(ctx context.Context, uid, phone string) error
|
||||
UserIndexUP
|
||||
}
|
||||
|
||||
type UserIndexUP interface {
|
||||
Index20241226001UP(ctx context.Context) (*mongo.Cursor, error)
|
||||
}
|
||||
|
||||
type BaseUserRepository interface {
|
||||
Insert(ctx context.Context, data *entity.User) error
|
||||
FindOne(ctx context.Context, id string) (*entity.User, error)
|
||||
Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error)
|
||||
Delete(ctx context.Context, id string) (int64, error)
|
||||
}
|
||||
|
||||
type UserQueryParams struct {
|
||||
AlarmCategory *member.AlarmType
|
||||
UserStatus *member.Status
|
||||
CreateStartTime *int64
|
||||
CreateEndTime *int64
|
||||
PageSize int64
|
||||
PageIndex int64
|
||||
}
|
||||
|
||||
type UpdateUserInfoRequest struct {
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL(可選)
|
||||
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||
Nickname *string `json:"nickname,omitempty"` // 暱稱(可選)
|
||||
GenderCode *int8 `json:"gender_code,omitempty"` // 性別代碼
|
||||
Birthdate *int64 `json:"birthdate,omitempty"` // 生日 (格式: 19930417)
|
||||
Address *string `json:"address,omitempty"` // 地址
|
||||
AlarmCategory *member.AlarmType `json:"alarm_category,omitempty"` // 警報類型
|
||||
UserStatus *member.Status `json:"user_status,omitempty"` // 用戶狀態
|
||||
PreferredLanguage *string `json:"preferred_language,omitempty"` // 使用語言
|
||||
Currency *string `json:"currency,omitempty"` // 使用幣種
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package repository
|
||||
|
||||
import "context"
|
||||
|
||||
type VerifyCodeRepository interface {
|
||||
IsVerifyCodeExist(ctx context.Context, loginID, checkType string) (string, error)
|
||||
SetVerifyCode(ctx context.Context, loginID, checkType, code string) error
|
||||
DelVerifyCode(ctx context.Context, loginID, checkType string) error
|
||||
}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/pkg/member/domain/member"
|
||||
)
|
||||
|
||||
// AccountUseCase 定義了帳號服務的操作方法
|
||||
type AccountUseCase interface {
|
||||
MemberUseCase
|
||||
BindingMemberUseCase
|
||||
VerifyMemberUseCase
|
||||
UIDGenerateUseCase
|
||||
}
|
||||
|
||||
type MemberUseCase interface {
|
||||
// CreateUserAccount 創建用戶帳號
|
||||
CreateUserAccount(ctx context.Context, req CreateLoginUserRequest) error
|
||||
// GetUIDByAccount 通過帳號取得 UID
|
||||
GetUIDByAccount(ctx context.Context, req GetUIDByAccountRequest) (GetUIDByAccountResponse, error)
|
||||
// GetUserAccountInfo 取得用戶帳號資訊
|
||||
GetUserAccountInfo(ctx context.Context, req GetUIDByAccountRequest) (GetAccountInfoResponse, error)
|
||||
// UpdateUserToken 更新用戶 Token (密碼)
|
||||
UpdateUserToken(ctx context.Context, req UpdateTokenRequest) error
|
||||
// UpdateUserInfo 更新用戶資訊
|
||||
UpdateUserInfo(ctx context.Context, req *UpdateUserInfoRequest) error
|
||||
// UpdateStatus 更新用戶狀態
|
||||
UpdateStatus(ctx context.Context, req UpdateStatusRequest) error
|
||||
// GetUserInfo 取得用戶資訊
|
||||
GetUserInfo(ctx context.Context, req GetUserInfoRequest) (UserInfo, error)
|
||||
// ListMember 取得會員列表
|
||||
ListMember(ctx context.Context, req ListUserInfoRequest) (ListUserInfoResponse, error)
|
||||
}
|
||||
|
||||
type BindingMemberUseCase interface {
|
||||
// BindUserInfo 綁定用戶信息
|
||||
BindUserInfo(ctx context.Context, req CreateUserInfoRequest) error
|
||||
// BindAccount 綁定帳號到 UID
|
||||
BindAccount(ctx context.Context, req BindingUser) (BindingUser, error)
|
||||
// BindVerifyEmail 驗證Email 後綁定到會員
|
||||
BindVerifyEmail(ctx context.Context, uid, email string) error
|
||||
// BindVerifyPhone 驗證 Phone 後綁定到會員
|
||||
BindVerifyPhone(ctx context.Context, uid, phone string) error
|
||||
}
|
||||
|
||||
// CreateUserInfoRequest 用於創建用戶詳細信息
|
||||
type CreateUserInfoRequest struct {
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL(可選)
|
||||
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||
Nickname *string `json:"nickname,omitempty"` // 暱稱(可選)
|
||||
GenderCode *int64 `json:"gender_code,omitempty"` // 性別代碼
|
||||
Birthdate *int64 `json:"birthdate,omitempty"` // 生日 (格式: 19930417)
|
||||
PhoneNumber *string `json:"phone_number,omitempty"` // 電話
|
||||
Email *string `json:"email,omitempty"` // 電話
|
||||
Address *string `json:"address,omitempty"` // 地址
|
||||
AlarmCategory member.AlarmType `json:"alarm_category"` // 警報類型
|
||||
UserStatus member.Status `json:"user_status"` // 用戶狀態
|
||||
PreferredLanguage string `json:"preferred_language"` // 使用語言
|
||||
Currency string `json:"currency"` // 使用幣種
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
CreateUserInfoRequest
|
||||
CreateTime int64 `json:"create_time"` // 創建時間
|
||||
UpdateTime int64 `json:"update_time"` // 更新時間
|
||||
}
|
||||
|
||||
type UpdateUserInfoRequest struct {
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
AvatarURL *string `json:"avatar_url,omitempty"` // 頭像 URL(可選)
|
||||
FullName *string `json:"full_name,omitempty"` // 用戶全名
|
||||
Nickname *string `json:"nickname,omitempty"` // 暱稱(可選)
|
||||
GenderCode *int8 `json:"gender_code,omitempty"` // 性別代碼
|
||||
Birthdate *int64 `json:"birthdate,omitempty"` // 生日 (格式: 19930417)
|
||||
Address *string `json:"address,omitempty"` // 地址
|
||||
AlarmCategory *member.AlarmType `json:"alarm_category,omitempty"` // 警報類型
|
||||
UserStatus *member.Status `json:"user_status,omitempty"` // 用戶狀態
|
||||
PreferredLanguage *string `json:"preferred_language,omitempty"` // 使用語言
|
||||
Currency *string `json:"currency,omitempty"` // 使用幣種
|
||||
}
|
||||
|
||||
type CreateLoginUserRequest struct {
|
||||
LoginID string `json:"login_id"` // 登錄 ID
|
||||
Platform member.Platform `json:"platform"` // 平台類型
|
||||
Token string `json:"token"` // 驗證 Token
|
||||
}
|
||||
|
||||
// BindingUser 用於綁定用戶帳號
|
||||
type BindingUser struct {
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
LoginID string `json:"login_id"` // 登錄 ID
|
||||
Type member.AccountType `json:"type"` // 綁定類型
|
||||
}
|
||||
|
||||
// GetUIDByAccountRequest 用於通過帳號獲取用戶 UID
|
||||
type GetUIDByAccountRequest struct {
|
||||
Account string `json:"account"` // 帳號
|
||||
}
|
||||
|
||||
// GetUIDByAccountResponse 用於返回帳號對應的 UID 信息
|
||||
type GetUIDByAccountResponse struct {
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
Account string `json:"account"` // 帳號
|
||||
}
|
||||
|
||||
// GetAccountInfoResponse 用於返回用戶帳號信息
|
||||
type GetAccountInfoResponse struct {
|
||||
Data CreateLoginUserRequest `json:"data"` // 登錄用戶信息
|
||||
}
|
||||
|
||||
// UpdateTokenRequest 用於更新用戶 Token
|
||||
type UpdateTokenRequest struct {
|
||||
Account string `json:"account"` // 帳號
|
||||
Token string `json:"token"` // 新 Token
|
||||
Platform int64 `json:"platform"` // 平台類型
|
||||
}
|
||||
|
||||
// GenerateRefreshCodeRequest 用於請求產生刷新代碼
|
||||
type GenerateRefreshCodeRequest struct {
|
||||
LoginID string `json:"login_id"` // 帳號
|
||||
CodeType member.GenerateCodeType `json:"code_type"` // 代碼類型
|
||||
}
|
||||
|
||||
// VerifyCode 用於表示驗證代碼
|
||||
type VerifyCode struct {
|
||||
VerifyCode string `json:"verify_code"` // 驗證碼
|
||||
}
|
||||
|
||||
// GenerateRefreshCodeResponse 用於返回生成的驗證代碼
|
||||
type GenerateRefreshCodeResponse struct {
|
||||
Data VerifyCode `json:"data"` // 驗證碼數據
|
||||
}
|
||||
|
||||
// VerifyRefreshCodeRequest 用於驗證刷新代碼
|
||||
type VerifyRefreshCodeRequest struct {
|
||||
LoginID string `json:"Login_id"` // 帳號
|
||||
CodeType member.GenerateCodeType `json:"code_type"` // 代碼類型
|
||||
VerifyCode string `json:"verify_code"` // 驗證碼
|
||||
}
|
||||
|
||||
// UpdateStatusRequest 用於更新用戶狀態
|
||||
type UpdateStatusRequest struct {
|
||||
UID string `json:"uid"` // 用戶 UID
|
||||
Status member.Status `json:"status"` // 用戶狀態
|
||||
}
|
||||
|
||||
// GetUserInfoRequest 用於請求取得用戶詳細信息
|
||||
type GetUserInfoRequest struct {
|
||||
UID string `json:"uid,omitempty"` // 用戶 UID
|
||||
NickName string `json:"nick_name,omitempty"` // 暱稱(可選)
|
||||
}
|
||||
|
||||
// GetUserInfoResponse 用於返回用戶詳細信息
|
||||
type GetUserInfoResponse struct {
|
||||
Data UserInfo `json:"data"` // 用戶信息
|
||||
}
|
||||
|
||||
// ListUserInfoRequest 用於查詢符合條件的用戶列表
|
||||
type ListUserInfoRequest struct {
|
||||
VerificationType *member.AccountType `json:"verification_type,omitempty"` // 驗證類型(可選)
|
||||
AlarmCategory *member.AlarmType `json:"alarm_category,omitempty"` // 警報類型(可選)
|
||||
UserStatus *member.Status `json:"user_status,omitempty"` // 用戶狀態(可選)
|
||||
CreateStartTime *int64 `json:"create_start_time,omitempty"` // 創建開始時間(可選)
|
||||
CreateEndTime *int64 `json:"create_end_time,omitempty"` // 創建結束時間(可選)
|
||||
PageSize int64 `json:"page_size"` // 每頁大小
|
||||
PageIndex int64 `json:"page_index"` // 當前頁索引
|
||||
}
|
||||
|
||||
// ListUserInfoResponse 用於返回查詢的用戶列表及分頁信息
|
||||
type ListUserInfoResponse struct {
|
||||
Data []UserInfo `json:"data"` // 用戶列表
|
||||
Page Pager `json:"page"` // 分頁信息
|
||||
}
|
||||
|
||||
// VerifyAuthResultRequest 用於請求驗證授權結果
|
||||
type VerifyAuthResultRequest struct {
|
||||
Token string `json:"token"` // 驗證 Token
|
||||
Account string `json:"account"` // 帳號
|
||||
}
|
||||
|
||||
// VerifyAuthResultResponse 用於返回授權驗證結果
|
||||
type VerifyAuthResultResponse struct {
|
||||
Status bool `json:"status"` // 驗證結果狀態
|
||||
}
|
||||
|
||||
// TwitterAccessTokenResponse 用於返回 Twitter 授權令牌
|
||||
type TwitterAccessTokenResponse struct {
|
||||
Token string `json:"token"` // 授權 Token
|
||||
}
|
||||
|
||||
type GoogleTokenInfo struct {
|
||||
Iss string `json:"iss"` // 發行者 (issuer) 通常為 "https://accounts.google.com"
|
||||
Sub string `json:"sub"` // 使用者唯一 ID
|
||||
Aud string `json:"aud"` // Audience,應該與你的 Client ID 匹配
|
||||
Exp string `json:"exp"` // 過期時間 (UNIX timestamp)
|
||||
Iat string `json:"iat"` // 發行時間 (UNIX timestamp)
|
||||
Email string `json:"email"` // 使用者的電子郵件
|
||||
EmailVerified string `json:"email_verified"` // 郵件是否已驗證
|
||||
Name string `json:"name"` // 使用者的名稱
|
||||
Picture string `json:"picture"` // 使用者的頭像 URL
|
||||
}
|
||||
|
||||
type LineAccessTokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
IDToken string `json:"id_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type LineUserProfile struct {
|
||||
UserID string `json:"userId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
PictureURL string `json:"pictureUrl"`
|
||||
StatusMessage string `json:"statusMessage"`
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package usecase
|
||||
|
||||
type Pager struct {
|
||||
Total int64 `json:"total"` // 總數量
|
||||
Size int64 `json:"size"` // 每頁大小
|
||||
Index int64 `json:"index"` // 當前頁索引
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package usecase
|
||||
|
||||
import "context"
|
||||
|
||||
type UIDGenerateUseCase interface {
|
||||
Generate(ctx context.Context) (string, error)
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package usecase
|
||||
|
||||
import "context"
|
||||
|
||||
type VerifyMemberUseCase interface {
|
||||
// GenerateRefreshCode 這個帳號驗證碼(十分鐘),通用的
|
||||
GenerateRefreshCode(ctx context.Context, req GenerateRefreshCodeRequest) (GenerateRefreshCodeResponse, error)
|
||||
// VerifyRefreshCode 驗證驗證碼,驗證完會刪除
|
||||
VerifyRefreshCode(ctx context.Context, req VerifyRefreshCodeRequest) error
|
||||
// CheckRefreshCode 驗證驗證碼,驗證完不會刪除
|
||||
CheckRefreshCode(ctx context.Context, req VerifyRefreshCodeRequest) error
|
||||
// VerifyPlatformAuthResult 驗證平台授權結果 -> 看密碼對不對
|
||||
VerifyPlatformAuthResult(ctx context.Context, req VerifyAuthResultRequest) (VerifyAuthResultResponse, error)
|
||||
GoogleVerify
|
||||
LineVerify
|
||||
}
|
||||
|
||||
type GoogleVerify interface {
|
||||
// VerifyGoogleAuthResult 驗證 Google 授權結果
|
||||
VerifyGoogleAuthResult(ctx context.Context, req VerifyAuthResultRequest) (GoogleTokenInfo, error)
|
||||
}
|
||||
|
||||
type LineVerify interface {
|
||||
// LineCodeToAccessToken 換取 AccessToken
|
||||
LineCodeToAccessToken(ctx context.Context, code string) (LineAccessTokenResponse, error)
|
||||
// LineGetProfileByAccessToken 用 Access Token 換取使用者的基本資料
|
||||
LineGetProfileByAccessToken(ctx context.Context, accessToken string) (*LineUserProfile, error)
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Validation errors
|
||||
var (
|
||||
ErrEmptyField = fmt.Errorf("field cannot be empty")
|
||||
ErrInvalidEmail = fmt.Errorf("invalid email format")
|
||||
ErrInvalidPhone = fmt.Errorf("invalid phone format")
|
||||
ErrPasswordTooShort = fmt.Errorf("password is too short")
|
||||
ErrPasswordTooLong = fmt.Errorf("password is too long")
|
||||
ErrInvalidLength = fmt.Errorf("invalid field length")
|
||||
)
|
||||
|
||||
// Email validation regex
|
||||
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
|
||||
// Phone validation regex (supports international formats)
|
||||
var phoneRegex = regexp.MustCompile(`^\+?[1-9]\d{2,14}$`)
|
||||
|
||||
// ValidateEmail validates an email address format
|
||||
func ValidateEmail(email string) error {
|
||||
if email == "" {
|
||||
return ErrEmptyField
|
||||
}
|
||||
|
||||
email = strings.TrimSpace(email)
|
||||
if !emailRegex.MatchString(email) {
|
||||
return ErrInvalidEmail
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePhone validates a phone number format
|
||||
func ValidatePhone(phone string) error {
|
||||
if phone == "" {
|
||||
return ErrEmptyField
|
||||
}
|
||||
|
||||
phone = strings.TrimSpace(phone)
|
||||
if !phoneRegex.MatchString(phone) {
|
||||
return ErrInvalidPhone
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePassword validates password strength
|
||||
func ValidatePassword(password string) error {
|
||||
if password == "" {
|
||||
return ErrEmptyField
|
||||
}
|
||||
|
||||
length := utf8.RuneCountInString(password)
|
||||
if length < MinPasswordLength {
|
||||
return ErrPasswordTooShort
|
||||
}
|
||||
|
||||
if length > MaxPasswordLength {
|
||||
return ErrPasswordTooLong
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateStringLength validates string length within specified bounds
|
||||
func ValidateStringLength(field, value string, min, max int) error {
|
||||
if value == "" {
|
||||
return ErrEmptyField
|
||||
}
|
||||
|
||||
length := utf8.RuneCountInString(value)
|
||||
if length < min || length > max {
|
||||
return ErrInvalidLength
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRequired validates that a required field is not empty
|
||||
func ValidateRequired(field, value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return ErrEmptyField
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateEmail(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid email",
|
||||
email: "test@example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid email with subdomain",
|
||||
email: "user@mail.example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid email - no @",
|
||||
email: "testexample.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid email - no domain",
|
||||
email: "test@",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty email",
|
||||
email: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "email with spaces",
|
||||
email: " test@example.com ",
|
||||
wantErr: false, // Should be trimmed
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateEmail(tt.email)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePhone(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
phone string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid phone with country code",
|
||||
phone: "+1234567890",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid phone without country code",
|
||||
phone: "1234567890",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid phone - too short",
|
||||
phone: "12",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid phone - contains letters",
|
||||
phone: "123-abc-4567",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty phone",
|
||||
phone: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "phone with spaces",
|
||||
phone: " +1234567890 ",
|
||||
wantErr: false, // Should be trimmed
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePhone(tt.phone)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePassword(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid password",
|
||||
password: "password123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "password too short",
|
||||
password: "123",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "password too long",
|
||||
password: string(make([]byte, MaxPasswordLength+1)),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty password",
|
||||
password: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "minimum length password",
|
||||
password: "12345678",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePassword(tt.password)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStringLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
min int
|
||||
max int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid length",
|
||||
value: "hello",
|
||||
min: 3,
|
||||
max: 10,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "too short",
|
||||
value: "hi",
|
||||
min: 3,
|
||||
max: 10,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "too long",
|
||||
value: "hello world",
|
||||
min: 3,
|
||||
max: 5,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
value: "",
|
||||
min: 1,
|
||||
max: 10,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateStringLength("test", tt.value, tt.min, tt.max)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRequired(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid value",
|
||||
value: "hello",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty value",
|
||||
value: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "whitespace only",
|
||||
value: " ",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "value with spaces",
|
||||
value: " hello ",
|
||||
wantErr: false, // Should be trimmed
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRequired("test", tt.value)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./pkg/member/domain/repository/account.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./pkg/member/domain/repository/account.go -destination=./pkg/member/mock/repository/account.go -package=mock
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
entity "backend/pkg/member/domain/entity"
|
||||
member "backend/pkg/member/domain/member"
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
mongo "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockAccountRepository is a mock of AccountRepository interface.
|
||||
type MockAccountRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAccountRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockAccountRepositoryMockRecorder is the mock recorder for MockAccountRepository.
|
||||
type MockAccountRepositoryMockRecorder struct {
|
||||
mock *MockAccountRepository
|
||||
}
|
||||
|
||||
// NewMockAccountRepository creates a new mock instance.
|
||||
func NewMockAccountRepository(ctrl *gomock.Controller) *MockAccountRepository {
|
||||
mock := &MockAccountRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockAccountRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAccountRepository) EXPECT() *MockAccountRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockAccountRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, id)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockAccountRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAccountRepository)(nil).Delete), ctx, id)
|
||||
}
|
||||
|
||||
// FindOne mocks base method.
|
||||
func (m *MockAccountRepository) FindOne(ctx context.Context, id string) (*entity.Account, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOne", ctx, id)
|
||||
ret0, _ := ret[0].(*entity.Account)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOne indicates an expected call of FindOne.
|
||||
func (mr *MockAccountRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAccountRepository)(nil).FindOne), ctx, id)
|
||||
}
|
||||
|
||||
// FindOneByAccount mocks base method.
|
||||
func (m *MockAccountRepository) FindOneByAccount(ctx context.Context, loginID string) (*entity.Account, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOneByAccount", ctx, loginID)
|
||||
ret0, _ := ret[0].(*entity.Account)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOneByAccount indicates an expected call of FindOneByAccount.
|
||||
func (mr *MockAccountRepositoryMockRecorder) FindOneByAccount(ctx, loginID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByAccount", reflect.TypeOf((*MockAccountRepository)(nil).FindOneByAccount), ctx, loginID)
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockAccountRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockAccountRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountRepository)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockAccountRepository) Insert(ctx context.Context, data *entity.Account) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Insert", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockAccountRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockAccountRepository)(nil).Insert), ctx, data)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockAccountRepository) Update(ctx context.Context, data *entity.Account) (*mongo.UpdateResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", ctx, data)
|
||||
ret0, _ := ret[0].(*mongo.UpdateResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockAccountRepositoryMockRecorder) Update(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAccountRepository)(nil).Update), ctx, data)
|
||||
}
|
||||
|
||||
// UpdateTokenByLoginID mocks base method.
|
||||
func (m *MockAccountRepository) UpdateTokenByLoginID(ctx context.Context, account, token string, platform member.Platform) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateTokenByLoginID", ctx, account, token, platform)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateTokenByLoginID indicates an expected call of UpdateTokenByLoginID.
|
||||
func (mr *MockAccountRepositoryMockRecorder) UpdateTokenByLoginID(ctx, account, token, platform any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTokenByLoginID", reflect.TypeOf((*MockAccountRepository)(nil).UpdateTokenByLoginID), ctx, account, token, platform)
|
||||
}
|
||||
|
||||
// MockAccountIndexUP is a mock of AccountIndexUP interface.
|
||||
type MockAccountIndexUP struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAccountIndexUPMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockAccountIndexUPMockRecorder is the mock recorder for MockAccountIndexUP.
|
||||
type MockAccountIndexUPMockRecorder struct {
|
||||
mock *MockAccountIndexUP
|
||||
}
|
||||
|
||||
// NewMockAccountIndexUP creates a new mock instance.
|
||||
func NewMockAccountIndexUP(ctrl *gomock.Controller) *MockAccountIndexUP {
|
||||
mock := &MockAccountIndexUP{ctrl: ctrl}
|
||||
mock.recorder = &MockAccountIndexUPMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAccountIndexUP) EXPECT() *MockAccountIndexUPMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockAccountIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockAccountIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountIndexUP)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./pkg/member/domain/repository/account_uid.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./pkg/member/domain/repository/account_uid.go -destination=./pkg/member/mock/repository/account_uid.go -package=mock
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
entity "backend/pkg/member/domain/entity"
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
mongo "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockAccountUIDRepository is a mock of AccountUIDRepository interface.
|
||||
type MockAccountUIDRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAccountUIDRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockAccountUIDRepositoryMockRecorder is the mock recorder for MockAccountUIDRepository.
|
||||
type MockAccountUIDRepositoryMockRecorder struct {
|
||||
mock *MockAccountUIDRepository
|
||||
}
|
||||
|
||||
// NewMockAccountUIDRepository creates a new mock instance.
|
||||
func NewMockAccountUIDRepository(ctrl *gomock.Controller) *MockAccountUIDRepository {
|
||||
mock := &MockAccountUIDRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockAccountUIDRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAccountUIDRepository) EXPECT() *MockAccountUIDRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockAccountUIDRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, id)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockAccountUIDRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAccountUIDRepository)(nil).Delete), ctx, id)
|
||||
}
|
||||
|
||||
// FindOne mocks base method.
|
||||
func (m *MockAccountUIDRepository) FindOne(ctx context.Context, id string) (*entity.AccountUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOne", ctx, id)
|
||||
ret0, _ := ret[0].(*entity.AccountUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOne indicates an expected call of FindOne.
|
||||
func (mr *MockAccountUIDRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAccountUIDRepository)(nil).FindOne), ctx, id)
|
||||
}
|
||||
|
||||
// FindUIDByLoginID mocks base method.
|
||||
func (m *MockAccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindUIDByLoginID", ctx, loginID)
|
||||
ret0, _ := ret[0].(*entity.AccountUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindUIDByLoginID indicates an expected call of FindUIDByLoginID.
|
||||
func (mr *MockAccountUIDRepositoryMockRecorder) FindUIDByLoginID(ctx, loginID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindUIDByLoginID", reflect.TypeOf((*MockAccountUIDRepository)(nil).FindUIDByLoginID), ctx, loginID)
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockAccountUIDRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockAccountUIDRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountUIDRepository)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockAccountUIDRepository) Insert(ctx context.Context, data *entity.AccountUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Insert", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockAccountUIDRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockAccountUIDRepository)(nil).Insert), ctx, data)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockAccountUIDRepository) Update(ctx context.Context, data *entity.AccountUID) (*mongo.UpdateResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", ctx, data)
|
||||
ret0, _ := ret[0].(*mongo.UpdateResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockAccountUIDRepositoryMockRecorder) Update(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAccountUIDRepository)(nil).Update), ctx, data)
|
||||
}
|
||||
|
||||
// MockAccountUIDIndexUP is a mock of AccountUIDIndexUP interface.
|
||||
type MockAccountUIDIndexUP struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAccountUIDIndexUPMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockAccountUIDIndexUPMockRecorder is the mock recorder for MockAccountUIDIndexUP.
|
||||
type MockAccountUIDIndexUPMockRecorder struct {
|
||||
mock *MockAccountUIDIndexUP
|
||||
}
|
||||
|
||||
// NewMockAccountUIDIndexUP creates a new mock instance.
|
||||
func NewMockAccountUIDIndexUP(ctrl *gomock.Controller) *MockAccountUIDIndexUP {
|
||||
mock := &MockAccountUIDIndexUP{ctrl: ctrl}
|
||||
mock.recorder = &MockAccountUIDIndexUPMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAccountUIDIndexUP) EXPECT() *MockAccountUIDIndexUPMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockAccountUIDIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockAccountUIDIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAccountUIDIndexUP)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./pkg/member/domain/repository/auto_id.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./pkg/member/domain/repository/auto_id.go -destination=./pkg/member/mock/repository/auto_id.go -package=mock
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
entity "backend/pkg/member/domain/entity"
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
mongo "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockAutoIDRepository is a mock of AutoIDRepository interface.
|
||||
type MockAutoIDRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAutoIDRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockAutoIDRepositoryMockRecorder is the mock recorder for MockAutoIDRepository.
|
||||
type MockAutoIDRepositoryMockRecorder struct {
|
||||
mock *MockAutoIDRepository
|
||||
}
|
||||
|
||||
// NewMockAutoIDRepository creates a new mock instance.
|
||||
func NewMockAutoIDRepository(ctrl *gomock.Controller) *MockAutoIDRepository {
|
||||
mock := &MockAutoIDRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockAutoIDRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAutoIDRepository) EXPECT() *MockAutoIDRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockAutoIDRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, id)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockAutoIDRepository)(nil).Delete), ctx, id)
|
||||
}
|
||||
|
||||
// FindOne mocks base method.
|
||||
func (m *MockAutoIDRepository) FindOne(ctx context.Context, id string) (*entity.AutoID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOne", ctx, id)
|
||||
ret0, _ := ret[0].(*entity.AutoID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOne indicates an expected call of FindOne.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockAutoIDRepository)(nil).FindOne), ctx, id)
|
||||
}
|
||||
|
||||
// GetNumFromUID mocks base method.
|
||||
func (m *MockAutoIDRepository) GetNumFromUID(uid string) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetNumFromUID", uid)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetNumFromUID indicates an expected call of GetNumFromUID.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) GetNumFromUID(uid any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNumFromUID", reflect.TypeOf((*MockAutoIDRepository)(nil).GetNumFromUID), uid)
|
||||
}
|
||||
|
||||
// GetUIDFromNum mocks base method.
|
||||
func (m *MockAutoIDRepository) GetUIDFromNum(num int64) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUIDFromNum", num)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUIDFromNum indicates an expected call of GetUIDFromNum.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) GetUIDFromNum(num any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUIDFromNum", reflect.TypeOf((*MockAutoIDRepository)(nil).GetUIDFromNum), num)
|
||||
}
|
||||
|
||||
// Inc mocks base method.
|
||||
func (m *MockAutoIDRepository) Inc(ctx context.Context, data *entity.AutoID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Inc", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Inc indicates an expected call of Inc.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) Inc(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Inc", reflect.TypeOf((*MockAutoIDRepository)(nil).Inc), ctx, data)
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockAutoIDRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAutoIDRepository)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockAutoIDRepository) Insert(ctx context.Context, data *entity.AutoID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Insert", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockAutoIDRepository)(nil).Insert), ctx, data)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockAutoIDRepository) Update(ctx context.Context, data *entity.AutoID) (*mongo.UpdateResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", ctx, data)
|
||||
ret0, _ := ret[0].(*mongo.UpdateResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockAutoIDRepositoryMockRecorder) Update(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockAutoIDRepository)(nil).Update), ctx, data)
|
||||
}
|
||||
|
||||
// MockAutoIDIndexUP is a mock of AutoIDIndexUP interface.
|
||||
type MockAutoIDIndexUP struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockAutoIDIndexUPMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockAutoIDIndexUPMockRecorder is the mock recorder for MockAutoIDIndexUP.
|
||||
type MockAutoIDIndexUPMockRecorder struct {
|
||||
mock *MockAutoIDIndexUP
|
||||
}
|
||||
|
||||
// NewMockAutoIDIndexUP creates a new mock instance.
|
||||
func NewMockAutoIDIndexUP(ctrl *gomock.Controller) *MockAutoIDIndexUP {
|
||||
mock := &MockAutoIDIndexUP{ctrl: ctrl}
|
||||
mock.recorder = &MockAutoIDIndexUPMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockAutoIDIndexUP) EXPECT() *MockAutoIDIndexUPMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockAutoIDIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockAutoIDIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockAutoIDIndexUP)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,342 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./pkg/member/domain/repository/user.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./pkg/member/domain/repository/user.go -destination=./pkg/member/mock/repository/user.go -package=mock
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
entity "backend/pkg/member/domain/entity"
|
||||
repository "backend/pkg/member/domain/repository"
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
mongo "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockUserRepository is a mock of UserRepository interface.
|
||||
type MockUserRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockUserRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockUserRepositoryMockRecorder is the mock recorder for MockUserRepository.
|
||||
type MockUserRepositoryMockRecorder struct {
|
||||
mock *MockUserRepository
|
||||
}
|
||||
|
||||
// NewMockUserRepository creates a new mock instance.
|
||||
func NewMockUserRepository(ctrl *gomock.Controller) *MockUserRepository {
|
||||
mock := &MockUserRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockUserRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockUserRepository) EXPECT() *MockUserRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockUserRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, id)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockUserRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUserRepository)(nil).Delete), ctx, id)
|
||||
}
|
||||
|
||||
// FindOne mocks base method.
|
||||
func (m *MockUserRepository) FindOne(ctx context.Context, id string) (*entity.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOne", ctx, id)
|
||||
ret0, _ := ret[0].(*entity.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOne indicates an expected call of FindOne.
|
||||
func (mr *MockUserRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockUserRepository)(nil).FindOne), ctx, id)
|
||||
}
|
||||
|
||||
// FindOneByNickName mocks base method.
|
||||
func (m *MockUserRepository) FindOneByNickName(ctx context.Context, nickName string) (*entity.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOneByNickName", ctx, nickName)
|
||||
ret0, _ := ret[0].(*entity.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOneByNickName indicates an expected call of FindOneByNickName.
|
||||
func (mr *MockUserRepositoryMockRecorder) FindOneByNickName(ctx, nickName any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByNickName", reflect.TypeOf((*MockUserRepository)(nil).FindOneByNickName), ctx, nickName)
|
||||
}
|
||||
|
||||
// FindOneByUID mocks base method.
|
||||
func (m *MockUserRepository) FindOneByUID(ctx context.Context, uid string) (*entity.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOneByUID", ctx, uid)
|
||||
ret0, _ := ret[0].(*entity.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOneByUID indicates an expected call of FindOneByUID.
|
||||
func (mr *MockUserRepositoryMockRecorder) FindOneByUID(ctx, uid any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOneByUID", reflect.TypeOf((*MockUserRepository)(nil).FindOneByUID), ctx, uid)
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockUserRepository) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockUserRepositoryMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockUserRepository)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockUserRepository) Insert(ctx context.Context, data *entity.User) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Insert", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockUserRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockUserRepository)(nil).Insert), ctx, data)
|
||||
}
|
||||
|
||||
// ListMembers mocks base method.
|
||||
func (m *MockUserRepository) ListMembers(ctx context.Context, params *repository.UserQueryParams) ([]*entity.User, int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListMembers", ctx, params)
|
||||
ret0, _ := ret[0].([]*entity.User)
|
||||
ret1, _ := ret[1].(int64)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// ListMembers indicates an expected call of ListMembers.
|
||||
func (mr *MockUserRepositoryMockRecorder) ListMembers(ctx, params any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMembers", reflect.TypeOf((*MockUserRepository)(nil).ListMembers), ctx, params)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockUserRepository) Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", ctx, data)
|
||||
ret0, _ := ret[0].(*mongo.UpdateResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockUserRepositoryMockRecorder) Update(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUserRepository)(nil).Update), ctx, data)
|
||||
}
|
||||
|
||||
// UpdateEmailVerifyStatus mocks base method.
|
||||
func (m *MockUserRepository) UpdateEmailVerifyStatus(ctx context.Context, uid, email string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateEmailVerifyStatus", ctx, uid, email)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateEmailVerifyStatus indicates an expected call of UpdateEmailVerifyStatus.
|
||||
func (mr *MockUserRepositoryMockRecorder) UpdateEmailVerifyStatus(ctx, uid, email any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEmailVerifyStatus", reflect.TypeOf((*MockUserRepository)(nil).UpdateEmailVerifyStatus), ctx, uid, email)
|
||||
}
|
||||
|
||||
// UpdatePhoneVerifyStatus mocks base method.
|
||||
func (m *MockUserRepository) UpdatePhoneVerifyStatus(ctx context.Context, uid, phone string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdatePhoneVerifyStatus", ctx, uid, phone)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdatePhoneVerifyStatus indicates an expected call of UpdatePhoneVerifyStatus.
|
||||
func (mr *MockUserRepositoryMockRecorder) UpdatePhoneVerifyStatus(ctx, uid, phone any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePhoneVerifyStatus", reflect.TypeOf((*MockUserRepository)(nil).UpdatePhoneVerifyStatus), ctx, uid, phone)
|
||||
}
|
||||
|
||||
// UpdateStatus mocks base method.
|
||||
func (m *MockUserRepository) UpdateStatus(ctx context.Context, uid string, status int32) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateStatus", ctx, uid, status)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateStatus indicates an expected call of UpdateStatus.
|
||||
func (mr *MockUserRepositoryMockRecorder) UpdateStatus(ctx, uid, status any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStatus", reflect.TypeOf((*MockUserRepository)(nil).UpdateStatus), ctx, uid, status)
|
||||
}
|
||||
|
||||
// UpdateUserDetailsByUID mocks base method.
|
||||
func (m *MockUserRepository) UpdateUserDetailsByUID(ctx context.Context, data *repository.UpdateUserInfoRequest) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateUserDetailsByUID", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateUserDetailsByUID indicates an expected call of UpdateUserDetailsByUID.
|
||||
func (mr *MockUserRepositoryMockRecorder) UpdateUserDetailsByUID(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserDetailsByUID", reflect.TypeOf((*MockUserRepository)(nil).UpdateUserDetailsByUID), ctx, data)
|
||||
}
|
||||
|
||||
// MockUserIndexUP is a mock of UserIndexUP interface.
|
||||
type MockUserIndexUP struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockUserIndexUPMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockUserIndexUPMockRecorder is the mock recorder for MockUserIndexUP.
|
||||
type MockUserIndexUPMockRecorder struct {
|
||||
mock *MockUserIndexUP
|
||||
}
|
||||
|
||||
// NewMockUserIndexUP creates a new mock instance.
|
||||
func NewMockUserIndexUP(ctrl *gomock.Controller) *MockUserIndexUP {
|
||||
mock := &MockUserIndexUP{ctrl: ctrl}
|
||||
mock.recorder = &MockUserIndexUPMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockUserIndexUP) EXPECT() *MockUserIndexUPMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Index20241226001UP mocks base method.
|
||||
func (m *MockUserIndexUP) Index20241226001UP(ctx context.Context) (*mongo.Cursor, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Index20241226001UP", ctx)
|
||||
ret0, _ := ret[0].(*mongo.Cursor)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Index20241226001UP indicates an expected call of Index20241226001UP.
|
||||
func (mr *MockUserIndexUPMockRecorder) Index20241226001UP(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index20241226001UP", reflect.TypeOf((*MockUserIndexUP)(nil).Index20241226001UP), ctx)
|
||||
}
|
||||
|
||||
// MockBaseUserRepository is a mock of BaseUserRepository interface.
|
||||
type MockBaseUserRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockBaseUserRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockBaseUserRepositoryMockRecorder is the mock recorder for MockBaseUserRepository.
|
||||
type MockBaseUserRepositoryMockRecorder struct {
|
||||
mock *MockBaseUserRepository
|
||||
}
|
||||
|
||||
// NewMockBaseUserRepository creates a new mock instance.
|
||||
func NewMockBaseUserRepository(ctrl *gomock.Controller) *MockBaseUserRepository {
|
||||
mock := &MockBaseUserRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockBaseUserRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockBaseUserRepository) EXPECT() *MockBaseUserRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Delete mocks base method.
|
||||
func (m *MockBaseUserRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Delete", ctx, id)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Delete indicates an expected call of Delete.
|
||||
func (mr *MockBaseUserRepositoryMockRecorder) Delete(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockBaseUserRepository)(nil).Delete), ctx, id)
|
||||
}
|
||||
|
||||
// FindOne mocks base method.
|
||||
func (m *MockBaseUserRepository) FindOne(ctx context.Context, id string) (*entity.User, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "FindOne", ctx, id)
|
||||
ret0, _ := ret[0].(*entity.User)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// FindOne indicates an expected call of FindOne.
|
||||
func (mr *MockBaseUserRepositoryMockRecorder) FindOne(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockBaseUserRepository)(nil).FindOne), ctx, id)
|
||||
}
|
||||
|
||||
// Insert mocks base method.
|
||||
func (m *MockBaseUserRepository) Insert(ctx context.Context, data *entity.User) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Insert", ctx, data)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Insert indicates an expected call of Insert.
|
||||
func (mr *MockBaseUserRepositoryMockRecorder) Insert(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockBaseUserRepository)(nil).Insert), ctx, data)
|
||||
}
|
||||
|
||||
// Update mocks base method.
|
||||
func (m *MockBaseUserRepository) Update(ctx context.Context, data *entity.User) (*mongo.UpdateResult, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Update", ctx, data)
|
||||
ret0, _ := ret[0].(*mongo.UpdateResult)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Update indicates an expected call of Update.
|
||||
func (mr *MockBaseUserRepositoryMockRecorder) Update(ctx, data any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockBaseUserRepository)(nil).Update), ctx, data)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./pkg/member/domain/repository/verify_code.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./pkg/member/domain/repository/verify_code.go -destination=./pkg/member/mock/repository/verify_code.go -package=mock
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockVerifyCodeRepository is a mock of VerifyCodeRepository interface.
|
||||
type MockVerifyCodeRepository struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockVerifyCodeRepositoryMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockVerifyCodeRepositoryMockRecorder is the mock recorder for MockVerifyCodeRepository.
|
||||
type MockVerifyCodeRepositoryMockRecorder struct {
|
||||
mock *MockVerifyCodeRepository
|
||||
}
|
||||
|
||||
// NewMockVerifyCodeRepository creates a new mock instance.
|
||||
func NewMockVerifyCodeRepository(ctrl *gomock.Controller) *MockVerifyCodeRepository {
|
||||
mock := &MockVerifyCodeRepository{ctrl: ctrl}
|
||||
mock.recorder = &MockVerifyCodeRepositoryMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockVerifyCodeRepository) EXPECT() *MockVerifyCodeRepositoryMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// DelVerifyCode mocks base method.
|
||||
func (m *MockVerifyCodeRepository) DelVerifyCode(ctx context.Context, loginID, checkType string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DelVerifyCode", ctx, loginID, checkType)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DelVerifyCode indicates an expected call of DelVerifyCode.
|
||||
func (mr *MockVerifyCodeRepositoryMockRecorder) DelVerifyCode(ctx, loginID, checkType any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelVerifyCode", reflect.TypeOf((*MockVerifyCodeRepository)(nil).DelVerifyCode), ctx, loginID, checkType)
|
||||
}
|
||||
|
||||
// IsVerifyCodeExist mocks base method.
|
||||
func (m *MockVerifyCodeRepository) IsVerifyCodeExist(ctx context.Context, loginID, checkType string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "IsVerifyCodeExist", ctx, loginID, checkType)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// IsVerifyCodeExist indicates an expected call of IsVerifyCodeExist.
|
||||
func (mr *MockVerifyCodeRepositoryMockRecorder) IsVerifyCodeExist(ctx, loginID, checkType any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsVerifyCodeExist", reflect.TypeOf((*MockVerifyCodeRepository)(nil).IsVerifyCodeExist), ctx, loginID, checkType)
|
||||
}
|
||||
|
||||
// SetVerifyCode mocks base method.
|
||||
func (m *MockVerifyCodeRepository) SetVerifyCode(ctx context.Context, loginID, checkType, code string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SetVerifyCode", ctx, loginID, checkType, code)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SetVerifyCode indicates an expected call of SetVerifyCode.
|
||||
func (mr *MockVerifyCodeRepositoryMockRecorder) SetVerifyCode(ctx, loginID, checkType, code any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVerifyCode", reflect.TypeOf((*MockVerifyCodeRepository)(nil).SetVerifyCode), ctx, loginID, checkType, code)
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./pkg/member/domain/usecase/generate_uid.go
|
||||
//
|
||||
// Generated by this command:
|
||||
//
|
||||
// mockgen -source=./pkg/member/domain/usecase/generate_uid.go -destination=./pkg/member/mock/usecase/generate_uid.go -package=mock
|
||||
//
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
reflect "reflect"
|
||||
|
||||
gomock "go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
// MockUIDGenerateUseCase is a mock of UIDGenerateUseCase interface.
|
||||
type MockUIDGenerateUseCase struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockUIDGenerateUseCaseMockRecorder
|
||||
isgomock struct{}
|
||||
}
|
||||
|
||||
// MockUIDGenerateUseCaseMockRecorder is the mock recorder for MockUIDGenerateUseCase.
|
||||
type MockUIDGenerateUseCaseMockRecorder struct {
|
||||
mock *MockUIDGenerateUseCase
|
||||
}
|
||||
|
||||
// NewMockUIDGenerateUseCase creates a new mock instance.
|
||||
func NewMockUIDGenerateUseCase(ctrl *gomock.Controller) *MockUIDGenerateUseCase {
|
||||
mock := &MockUIDGenerateUseCase{ctrl: ctrl}
|
||||
mock.recorder = &MockUIDGenerateUseCaseMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockUIDGenerateUseCase) EXPECT() *MockUIDGenerateUseCaseMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Generate mocks base method.
|
||||
func (m *MockUIDGenerateUseCase) Generate(ctx context.Context) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Generate", ctx)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Generate indicates an expected call of Generate.
|
||||
func (mr *MockUIDGenerateUseCaseMockRecorder) Generate(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Generate", reflect.TypeOf((*MockUIDGenerateUseCase)(nil).Generate), ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/member"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
"backend/pkg/library/mongo"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type AccountRepositoryParam struct {
|
||||
Conf *mongo.Conf
|
||||
CacheConf cache.CacheConf
|
||||
DBOpts []mon.Option
|
||||
CacheOpts []cache.Option
|
||||
}
|
||||
|
||||
type AccountRepository struct {
|
||||
DB mongo.DocumentDBWithCacheUseCase
|
||||
}
|
||||
|
||||
func NewAccountRepository(param AccountRepositoryParam) repository.AccountRepository {
|
||||
e := entity.Account{}
|
||||
documentDB, err := mongo.MustDocumentDBWithCache(
|
||||
param.Conf,
|
||||
e.CollectionName(),
|
||||
param.CacheConf,
|
||||
param.DBOpts,
|
||||
param.CacheOpts,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &AccountRepository{
|
||||
DB: documentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) Insert(ctx context.Context, data *entity.Account) error {
|
||||
if data.ID.IsZero() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.ID = bson.NewObjectID()
|
||||
data.CreateAt = &now
|
||||
data.UpdateAt = &now
|
||||
}
|
||||
rk := domain.GetAccountRedisKey(data.ID.Hex())
|
||||
_, err := repo.DB.InsertOne(ctx, rk, data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) FindOne(ctx context.Context, id string) (*entity.Account, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
var data entity.Account
|
||||
rk := domain.GetAccountRedisKey(id)
|
||||
err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid})
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) Update(ctx context.Context, data *entity.Account) (*mongodriver.UpdateResult, error) {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.UpdateAt = &now
|
||||
|
||||
rk := domain.GetAccountRedisKey(data.ID.Hex())
|
||||
res, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, bson.M{"$set": data})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidObjectID
|
||||
}
|
||||
rk := domain.GetAccountRedisKey(id)
|
||||
|
||||
return repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid})
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) FindOneByAccount(ctx context.Context, loginID string) (*entity.Account, error) {
|
||||
// todo: 之後需要同步快取
|
||||
var data entity.Account
|
||||
err := repo.DB.GetClient().FindOne(ctx, &data, bson.M{"login_id": loginID})
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) UpdateTokenByLoginID(ctx context.Context, account string, token string, platform member.Platform) error {
|
||||
// todo: 之後需要同步快取
|
||||
filter := bson.M{"login_id": account, "platform": platform.ToInt64()}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"token": token,
|
||||
"update_at": time.Now().UTC().UnixNano(),
|
||||
},
|
||||
}
|
||||
var data entity.Account
|
||||
err := repo.DB.GetClient().FindOne(ctx, &data, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rk := domain.GetAccountRedisKey(data.ID.Hex())
|
||||
|
||||
modify, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, update)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if modify.MatchedCount == 0 {
|
||||
return ErrNotFound // 自定義的錯誤表示未找到記錄
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *AccountRepository) Index20241226001UP(ctx context.Context) (*mongodriver.Cursor, error) {
|
||||
// 等價於 db.account.createIndex({ "login_id": 1, "platform": 1}, {unique: true})
|
||||
repo.DB.PopulateMultiIndex(ctx, []string{
|
||||
"login_id",
|
||||
"platform",
|
||||
}, []int32{1, 1}, true)
|
||||
|
||||
// 等價於 db.account.createIndex({"create_at": 1})
|
||||
repo.DB.PopulateIndex(ctx, "create_at", 1, false)
|
||||
|
||||
return repo.DB.GetClient().Indexes().List(ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,355 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"backend/pkg/member/domain/member"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
mgo "backend/pkg/library/mongo"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
func SetupTestAccountRepository(db string) (repository.AccountRepository, func(), error) {
|
||||
h, p, tearDown, err := startMongoContainer()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s, _ := miniredis.Run()
|
||||
|
||||
conf := &mgo.Conf{
|
||||
Schema: Schema,
|
||||
Host: fmt.Sprintf("%s:%s", h, p),
|
||||
Database: db,
|
||||
MaxStaleness: 300,
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 100,
|
||||
MaxConnIdleTime: 300,
|
||||
Compressors: []string{},
|
||||
EnableStandardReadWriteSplitMode: false,
|
||||
ConnectTimeoutMs: 3000,
|
||||
}
|
||||
|
||||
cacheConf := cache.CacheConf{
|
||||
cache.NodeConf{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: s.Addr(),
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
|
||||
cacheOpts := []cache.Option{
|
||||
cache.WithExpiry(1000 * time.Microsecond),
|
||||
cache.WithNotFoundExpiry(1000 * time.Microsecond),
|
||||
}
|
||||
|
||||
param := AccountRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: cacheConf,
|
||||
CacheOpts: cacheOpts,
|
||||
}
|
||||
repo := NewAccountRepository(param)
|
||||
_, _ = repo.Index20241226001UP(context.Background())
|
||||
|
||||
return repo, tearDown, nil
|
||||
}
|
||||
|
||||
func TestAccountModel_Insert(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
account *entity.Account
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid account insert",
|
||||
account: &entity.Account{
|
||||
LoginID: "testuser1",
|
||||
Token: "testtoken1",
|
||||
Platform: 1,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 插入測試帳戶
|
||||
err := repo.Insert(context.Background(), tt.account)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err, "插入操作應該成功")
|
||||
|
||||
// 檢查是否生成了 ObjectID 和時間戳
|
||||
assert.NotZero(t, tt.account.ID, "ID 應該被生成")
|
||||
assert.NotNil(t, tt.account.CreateAt, "CreateAt 應該被設置")
|
||||
assert.NotNil(t, tt.account.UpdateAt, "UpdateAt 應該被設置")
|
||||
|
||||
// 檢查插入的時間是否合理
|
||||
now := time.Now().UTC().UnixNano()
|
||||
assert.LessOrEqual(t, *tt.account.CreateAt, now, "CreateAt 應在當前時間之前")
|
||||
assert.LessOrEqual(t, *tt.account.UpdateAt, now, "UpdateAt 應在當前時間之前")
|
||||
|
||||
// 確認插入的資料
|
||||
insertedAccount, err := repo.FindOne(context.Background(), tt.account.ID.Hex())
|
||||
assert.NoError(t, err, "應該可以找到插入的帳號資料")
|
||||
assert.Equal(t, tt.account.LoginID, insertedAccount.LoginID, "LoginID 應相同")
|
||||
assert.Equal(t, tt.account.Token, insertedAccount.Token, "Token 應相同")
|
||||
assert.Equal(t, tt.account.Platform, insertedAccount.Platform, "Platform 應相同")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountModel_FindOne(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountRepository("testDB")
|
||||
assert.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
// 插入測試帳戶
|
||||
account := &entity.Account{
|
||||
LoginID: "testuser1",
|
||||
Token: "testtoken1",
|
||||
Platform: 1,
|
||||
}
|
||||
err = repo.Insert(context.TODO(), account)
|
||||
assert.NoError(t, err, "插入應成功")
|
||||
nid := primitive.NewObjectID()
|
||||
t.Logf("Inserted account ID: %s, nid:%s", account.ID.Hex(), nid.Hex())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expectError bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Valid ID",
|
||||
id: account.ID.Hex(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Non-existent ID",
|
||||
id: nid.Hex(),
|
||||
expectError: true,
|
||||
expectedErr: ErrNotFound,
|
||||
},
|
||||
{
|
||||
name: "Invalid ID format",
|
||||
id: "invalid-id",
|
||||
expectError: true,
|
||||
expectedErr: ErrInvalidObjectID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := repo.FindOne(context.TODO(), tt.id)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, account.LoginID, result.LoginID)
|
||||
assert.Equal(t, account.Token, result.Token)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountModel_Update(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountRepository("testDB")
|
||||
assert.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
// 插入測試帳戶
|
||||
account := &entity.Account{
|
||||
LoginID: "testuser1",
|
||||
Token: "testtoken1",
|
||||
Platform: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), account)
|
||||
assert.NoError(t, err, "插入應成功")
|
||||
|
||||
// 更新測試
|
||||
newToken := "updatedToken"
|
||||
account.Token = newToken
|
||||
|
||||
_, err = repo.Update(context.Background(), account)
|
||||
assert.NoError(t, err, "更新應成功")
|
||||
|
||||
// 驗證更新結果
|
||||
updatedAccount, err := repo.FindOne(context.Background(), account.ID.Hex())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newToken, updatedAccount.Token, "Token 應更新")
|
||||
}
|
||||
|
||||
func TestAccountModel_Delete(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountRepository("testDB")
|
||||
assert.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
// 插入測試帳戶
|
||||
account := &entity.Account{
|
||||
LoginID: "testuser1",
|
||||
Token: "testtoken1",
|
||||
Platform: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), account)
|
||||
assert.NoError(t, err, "插入應成功")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expectError bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Valid delete",
|
||||
id: account.ID.Hex(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid ID format",
|
||||
id: "invalid-id",
|
||||
expectError: true,
|
||||
expectedErr: ErrInvalidObjectID,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
deletedCount, err := repo.Delete(context.Background(), tt.id)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.expectedErr, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), deletedCount, "刪除應成功")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountModel_FindOneByAccount(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountRepository("testDB")
|
||||
assert.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
// 插入測試帳戶
|
||||
account := &entity.Account{
|
||||
LoginID: "testuser1",
|
||||
Token: "testtoken1",
|
||||
Platform: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), account)
|
||||
assert.NoError(t, err, "插入應成功")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
loginID string
|
||||
expectedErr error
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Account Found",
|
||||
loginID: "testuser1",
|
||||
expectedErr: nil,
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "Account Not Found",
|
||||
loginID: "nonexistentuser",
|
||||
expectedErr: ErrNotFound,
|
||||
expectFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := repo.FindOneByAccount(context.Background(), tt.loginID)
|
||||
if tt.expectFound {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, account.LoginID, result.LoginID)
|
||||
assert.Equal(t, account.Token, result.Token)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, tt.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountModel_UpdateTokenByLoginID(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountRepository("testDB")
|
||||
assert.NoError(t, err)
|
||||
defer tearDown()
|
||||
|
||||
// 插入測試帳戶
|
||||
account := &entity.Account{
|
||||
LoginID: "testuser2",
|
||||
Token: "initialtoken",
|
||||
Platform: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), account)
|
||||
assert.NoError(t, err, "插入應成功")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
loginID string
|
||||
newToken string
|
||||
platform int64
|
||||
expectedErr error
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Update Token",
|
||||
loginID: "testuser2",
|
||||
newToken: "newtoken123",
|
||||
platform: 1,
|
||||
expectedErr: nil,
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "Account Not Found for Update",
|
||||
loginID: "nonexistentuser",
|
||||
newToken: "newtoken456",
|
||||
platform: 1,
|
||||
expectedErr: ErrNotFound,
|
||||
expectFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := repo.UpdateTokenByLoginID(context.Background(), tt.loginID, tt.newToken, member.Platform(tt.platform))
|
||||
if tt.expectFound {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 驗證更新後的 token 值
|
||||
updatedAccount, findErr := repo.FindOneByAccount(context.Background(), tt.loginID)
|
||||
assert.NoError(t, findErr)
|
||||
assert.Equal(t, tt.newToken, updatedAccount.Token)
|
||||
} else {
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, tt.expectedErr))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
"backend/pkg/library/mongo"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
)
|
||||
|
||||
type AccountUIDRepositoryParam struct {
|
||||
Conf *mongo.Conf
|
||||
CacheConf cache.CacheConf
|
||||
DBOpts []mon.Option
|
||||
CacheOpts []cache.Option
|
||||
}
|
||||
|
||||
type AccountUIDRepository struct {
|
||||
DB mongo.DocumentDBWithCacheUseCase
|
||||
}
|
||||
|
||||
func NewAccountUIDRepository(param AccountUIDRepositoryParam) repository.AccountUIDRepository {
|
||||
e := entity.AccountUID{}
|
||||
documentDB, err := mongo.MustDocumentDBWithCache(
|
||||
param.Conf,
|
||||
e.CollectionName(),
|
||||
param.CacheConf,
|
||||
param.DBOpts,
|
||||
param.CacheOpts,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &AccountUIDRepository{
|
||||
DB: documentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AccountUIDRepository) Insert(ctx context.Context, data *entity.AccountUID) error {
|
||||
if data.ID.IsZero() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.ID = bson.NewObjectID()
|
||||
data.CreateAt = &now
|
||||
data.UpdateAt = &now
|
||||
}
|
||||
rk := domain.GetAccountUIDRedisKey(data.ID.Hex())
|
||||
_, err := repo.DB.InsertOne(ctx, rk, data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *AccountUIDRepository) FindOne(ctx context.Context, id string) (*entity.AccountUID, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
var data entity.AccountUID
|
||||
rk := domain.GetAccountUIDRedisKey(id)
|
||||
err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid})
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AccountUIDRepository) Update(ctx context.Context, data *entity.AccountUID) (*mongodriver.UpdateResult, error) {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.UpdateAt = &now
|
||||
|
||||
rk := domain.GetAccountUIDRedisKey(data.ID.Hex())
|
||||
res, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, bson.M{"$set": data})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (repo *AccountUIDRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidObjectID
|
||||
}
|
||||
rk := domain.GetAccountUIDRedisKey(id)
|
||||
|
||||
return repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid})
|
||||
}
|
||||
|
||||
func (repo *AccountUIDRepository) FindUIDByLoginID(ctx context.Context, loginID string) (*entity.AccountUID, error) {
|
||||
var data entity.AccountUID
|
||||
|
||||
err := repo.DB.GetClient().FindOne(ctx, &data, bson.M{"login_id": loginID})
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AccountUIDRepository) Index20241226001UP(ctx context.Context) (*mongodriver.Cursor, error) {
|
||||
// 等價於 db.account_uid_binding.createIndex({"login_id": 1}, {unique: true})
|
||||
repo.DB.PopulateIndex(ctx, "login_id", 1, true)
|
||||
|
||||
// 等價於 db.account_uid_binding.createIndex({"uid": 1})
|
||||
repo.DB.PopulateIndex(ctx, "uid", 1, false)
|
||||
|
||||
// 等價於 db.account_uid_binding.createIndex({"create_at": 1})
|
||||
repo.DB.PopulateIndex(ctx, "create_at", 1, false)
|
||||
|
||||
return repo.DB.GetClient().Indexes().List(ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
mgo "backend/pkg/library/mongo"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
func SetupTestAccountUIDRepository(db string) (repository.AccountUIDRepository, func(), error) {
|
||||
h, p, tearDown, err := startMongoContainer()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s, _ := miniredis.Run()
|
||||
|
||||
conf := &mgo.Conf{
|
||||
Schema: Schema,
|
||||
Host: fmt.Sprintf("%s:%s", h, p),
|
||||
Database: db,
|
||||
MaxStaleness: 300,
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 100,
|
||||
MaxConnIdleTime: 300,
|
||||
Compressors: []string{},
|
||||
EnableStandardReadWriteSplitMode: false,
|
||||
ConnectTimeoutMs: 3000,
|
||||
}
|
||||
|
||||
cacheConf := cache.CacheConf{
|
||||
cache.NodeConf{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: s.Addr(),
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
|
||||
cacheOpts := []cache.Option{
|
||||
cache.WithExpiry(1000 * time.Microsecond),
|
||||
cache.WithNotFoundExpiry(1000 * time.Microsecond),
|
||||
}
|
||||
|
||||
param := AccountUIDRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: cacheConf,
|
||||
CacheOpts: cacheOpts,
|
||||
}
|
||||
|
||||
repo := NewAccountUIDRepository(param)
|
||||
_, _ = repo.Index20241226001UP(context.Background())
|
||||
|
||||
return repo, tearDown, nil
|
||||
}
|
||||
|
||||
func TestDefaultAccountUidModel_Insert(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountUIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
accountUid *entity.AccountUID
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid AccountUid insert",
|
||||
accountUid: &entity.AccountUID{
|
||||
LoginID: "testlogin1",
|
||||
UID: "testuid1",
|
||||
Type: 1,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Insert with missing UID",
|
||||
accountUid: &entity.AccountUID{
|
||||
LoginID: "testlogin2",
|
||||
Type: 2,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// 執行插入測試帳戶 UID
|
||||
err := repo.Insert(context.Background(), tt.accountUid)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "插入操作應該失敗")
|
||||
} else {
|
||||
assert.NoError(t, err, "插入操作應該成功")
|
||||
|
||||
// 驗證 ObjectID 和時間戳是否生成
|
||||
assert.NotZero(t, tt.accountUid.ID, "應生成 ID")
|
||||
assert.NotNil(t, tt.accountUid.CreateAt, "CreateAt 應被設置")
|
||||
assert.NotNil(t, tt.accountUid.UpdateAt, "UpdateAt 應被設置")
|
||||
|
||||
// 驗證插入的時間是否合理
|
||||
now := time.Now().UTC().UnixNano()
|
||||
assert.LessOrEqual(t, *tt.accountUid.CreateAt, now, "CreateAt 應在當前時間之前")
|
||||
assert.LessOrEqual(t, *tt.accountUid.UpdateAt, now, "UpdateAt 應在當前時間之前")
|
||||
|
||||
// 驗證插入的資料是否正確
|
||||
insertedAccountUid, err := repo.FindOne(context.Background(), tt.accountUid.ID.Hex())
|
||||
assert.NoError(t, err, "應該可以找到插入的帳號 UID 資料")
|
||||
assert.Equal(t, tt.accountUid.LoginID, insertedAccountUid.LoginID, "LoginID 應相同")
|
||||
assert.Equal(t, tt.accountUid.UID, insertedAccountUid.UID, "UID 應相同")
|
||||
assert.Equal(t, tt.accountUid.Type, insertedAccountUid.Type, "Type 應相同")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountUidModel_FindOne(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountUIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 準備測試資料
|
||||
accountUid := &entity.AccountUID{
|
||||
LoginID: "testlogin",
|
||||
UID: "testuid",
|
||||
Type: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), accountUid)
|
||||
assert.NoError(t, err, "應成功插入測試資料")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expectError bool
|
||||
expectedUID string
|
||||
}{
|
||||
{
|
||||
name: "Valid FindOne",
|
||||
id: accountUid.ID.Hex(),
|
||||
expectError: false,
|
||||
expectedUID: accountUid.UID,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalid_id",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Non-existent ObjectID",
|
||||
id: primitive.NewObjectID().Hex(),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := repo.FindOne(context.Background(), tt.id)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedUID, result.UID, "找到的 UID 應符合預期")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAccountUidModel_Update(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountUIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 準備測試資料
|
||||
accountUid := &entity.AccountUID{
|
||||
LoginID: "testlogin",
|
||||
UID: "testuid",
|
||||
Type: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), accountUid)
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedUID := "updatedUID"
|
||||
accountUid.UID = updatedUID
|
||||
|
||||
// 執行更新操作
|
||||
_, err = repo.Update(context.Background(), accountUid)
|
||||
assert.NoError(t, err, "應成功更新資料")
|
||||
|
||||
// 確認更新結果
|
||||
updatedAccountUid, err := repo.FindOne(context.Background(), accountUid.ID.Hex())
|
||||
assert.NoError(t, err, "應能找到更新後的資料")
|
||||
assert.Equal(t, updatedUID, updatedAccountUid.UID, "更新後的 UID 應符合預期")
|
||||
}
|
||||
|
||||
func TestDefaultAccountUidModel_Delete(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountUIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 準備測試資料
|
||||
accountUid := &entity.AccountUID{
|
||||
LoginID: "testlogin",
|
||||
UID: "testuid",
|
||||
Type: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), accountUid)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 執行刪除操作
|
||||
count, err := repo.Delete(context.Background(), accountUid.ID.Hex())
|
||||
assert.NoError(t, err, "應成功刪除資料")
|
||||
assert.Equal(t, int64(1), count, "刪除數量應為 1")
|
||||
|
||||
// 確認資料已被刪除
|
||||
_, err = repo.FindOne(context.Background(), accountUid.ID.Hex())
|
||||
assert.Error(t, err, "應無法找到已刪除的資料")
|
||||
assert.Equal(t, ErrNotFound, err, "應返回 ErrNotFound 錯誤")
|
||||
}
|
||||
|
||||
func TestCustomAccountUidModel_FindUIDByLoginID(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAccountUIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 準備測試資料
|
||||
accountUid := &entity.AccountUID{
|
||||
LoginID: "testloginid",
|
||||
UID: "testuid",
|
||||
Type: 1,
|
||||
}
|
||||
err = repo.Insert(context.Background(), accountUid)
|
||||
assert.NoError(t, err, "應成功插入測試資料")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
loginID string
|
||||
expectError bool
|
||||
expectedUID string
|
||||
}{
|
||||
{
|
||||
name: "Valid FindUIDByLoginID",
|
||||
loginID: "testloginid",
|
||||
expectError: false,
|
||||
expectedUID: "testuid",
|
||||
},
|
||||
{
|
||||
name: "Non-existent LoginID",
|
||||
loginID: "nonexistent",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := repo.FindUIDByLoginID(context.Background(), tt.loginID)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrNotFound, err, "應返回 ErrNotFound 錯誤")
|
||||
} else {
|
||||
assert.NoError(t, err, "應成功找到符合的記錄")
|
||||
assert.Equal(t, tt.expectedUID, result.UID, "找到的 UID 應符合預期")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
GIDLib "code.30cm.net/digimon/library-go/utils/invited_code"
|
||||
|
||||
"backend/pkg/library/mongo"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type AutoIDRepositoryParam struct {
|
||||
Conf *mongo.Conf
|
||||
DBOpts []mon.Option
|
||||
}
|
||||
|
||||
type AutoIDRepository struct {
|
||||
DB mongo.DocumentDBUseCase
|
||||
UIDTrans GIDLib.ConvertUseCase
|
||||
}
|
||||
|
||||
func NewAutoIDRepository(param AutoIDRepositoryParam) repository.AutoIDRepository {
|
||||
e := entity.AutoID{}
|
||||
documentDB, err := mongo.NewDocumentDB(param.Conf, e.CollectionName(), param.DBOpts...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &AutoIDRepository{
|
||||
DB: documentDB,
|
||||
UIDTrans: GIDLib.MustConverter(10, 8, GIDLib.ConvertTable),
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) Insert(ctx context.Context, data *entity.AutoID) error {
|
||||
if data.ID.IsZero() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.ID = bson.NewObjectID()
|
||||
data.CreateAt = &now
|
||||
data.UpdateAt = &now
|
||||
}
|
||||
_, err := repo.DB.GetClient().InsertOne(ctx, data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) FindOne(ctx context.Context, id string) (*entity.AutoID, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
var data entity.AutoID
|
||||
|
||||
err = repo.DB.GetClient().FindOne(ctx, &data, bson.M{"_id": oid})
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) Update(ctx context.Context, data *entity.AutoID) (*mongodriver.UpdateResult, error) {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.UpdateAt = &now
|
||||
|
||||
res, err := repo.DB.GetClient().UpdateOne(ctx, bson.M{"_id": data.ID}, bson.M{"$set": data})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
res, err := repo.DB.GetClient().DeleteOne(ctx, bson.M{"_id": oid})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) Inc(ctx context.Context, data *entity.AutoID) error {
|
||||
// 定義查詢的條件
|
||||
filter := bson.M{"name": "auto_id"}
|
||||
|
||||
// 定義更新的操作,包括自增和更新時間
|
||||
update := bson.M{
|
||||
"$inc": bson.M{"counter": 1}, // 自增 counter
|
||||
"$set": bson.M{"updateAt": time.Now().UTC().UnixNano()}, // 設置 updateAt 為當前時間
|
||||
}
|
||||
// 使用 FindOneAndUpdate 並將結果解碼到 data 中
|
||||
err := repo.DB.GetClient().FindOneAndUpdate(ctx, &data, filter, update,
|
||||
options.FindOneAndUpdate().SetUpsert(true),
|
||||
options.FindOneAndUpdate().SetReturnDocument(options.After))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) Index20241226001UP(ctx context.Context) (*mongodriver.Cursor, error) {
|
||||
// db.count.createIndex({ "name": 1 }, { unique: true });
|
||||
repo.DB.PopulateIndex(ctx, "name", 1, true)
|
||||
|
||||
return repo.DB.GetClient().Indexes().List(ctx)
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) GetUIDFromNum(num int64) (string, error) {
|
||||
return repo.UIDTrans.EncodeFromNum(num)
|
||||
}
|
||||
|
||||
func (repo *AutoIDRepository) GetNumFromUID(uid string) (int64, error) {
|
||||
return repo.UIDTrans.DecodeFromCode(uid)
|
||||
}
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
mgo "backend/pkg/library/mongo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func SetupTestAutoIDRepository(db string) (repository.AutoIDRepository, func(), error) {
|
||||
h, p, tearDown, err := startMongoContainer()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
conf := &mgo.Conf{
|
||||
Schema: Schema,
|
||||
Host: fmt.Sprintf("%s:%s", h, p),
|
||||
Database: db,
|
||||
MaxStaleness: 300,
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 100,
|
||||
MaxConnIdleTime: 300,
|
||||
Compressors: []string{},
|
||||
EnableStandardReadWriteSplitMode: false,
|
||||
ConnectTimeoutMs: 3000,
|
||||
}
|
||||
|
||||
param := AutoIDRepositoryParam{
|
||||
Conf: conf,
|
||||
}
|
||||
|
||||
repo := NewAutoIDRepository(param)
|
||||
_, _ = repo.Index20241226001UP(context.Background())
|
||||
|
||||
return repo, tearDown, nil
|
||||
}
|
||||
|
||||
func TestDefaultAutoIDModel_Insert(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAutoIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data *entity.AutoID
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid AutoID insert",
|
||||
data: &entity.AutoID{
|
||||
Name: "testCounter",
|
||||
Counter: 100,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := repo.Insert(context.Background(), tt.data)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err, "插入操作應該成功")
|
||||
|
||||
// 檢查生成的 ID 和時間戳
|
||||
assert.NotZero(t, tt.data.ID, "ID 應該被生成")
|
||||
assert.NotNil(t, tt.data.CreateAt, "CreateAt 應該被設置")
|
||||
assert.NotNil(t, tt.data.UpdateAt, "UpdateAt 應該被設置")
|
||||
|
||||
// 確認插入的資料是否正確
|
||||
now := time.Now().UTC().UnixNano()
|
||||
assert.LessOrEqual(t, *tt.data.CreateAt, now, "CreateAt 應在當前時間之前")
|
||||
assert.LessOrEqual(t, *tt.data.UpdateAt, now, "UpdateAt 應在當前時間之前")
|
||||
|
||||
insertedAutoID, err := repo.FindOne(context.Background(), tt.data.ID.Hex())
|
||||
assert.NoError(t, err, "應該可以找到插入的資料")
|
||||
assert.Equal(t, tt.data.Name, insertedAutoID.Name, "Name 應相同")
|
||||
assert.Equal(t, tt.data.Counter, insertedAutoID.Counter, "Counter 應相同")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAutoIDModel_FindOne(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAutoIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
inserted := &entity.AutoID{
|
||||
Name: "findTestCounter",
|
||||
Counter: 200,
|
||||
}
|
||||
err = repo.Insert(context.Background(), inserted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expectError bool
|
||||
expectedID string
|
||||
}{
|
||||
{
|
||||
name: "Valid FindOne by ID",
|
||||
id: inserted.ID.Hex(),
|
||||
expectError: false,
|
||||
expectedID: inserted.ID.Hex(),
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalidObjectID",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := repo.FindOne(context.Background(), tt.id)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedID, result.ID.Hex(), "ID 應相同")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAutoIDModel_Update(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAutoIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
inserted := &entity.AutoID{
|
||||
Name: "updateTestCounter",
|
||||
Counter: 300,
|
||||
}
|
||||
err = repo.Insert(context.Background(), inserted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedData := *inserted // 創建副本進行更新
|
||||
updatedData.Counter = 400
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data *entity.AutoID
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Update",
|
||||
data: &updatedData,
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := repo.Update(context.Background(), tt.data)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
// 驗證更新後的 Counter
|
||||
updatedAutoID, err := repo.FindOne(context.Background(), tt.data.ID.Hex())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.data.Counter, updatedAutoID.Counter, "Counter 應相同")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultAutoIDModel_Delete(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAutoIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
inserted := &entity.AutoID{
|
||||
Name: "deleteTestCounter",
|
||||
Counter: 500,
|
||||
}
|
||||
err = repo.Insert(context.Background(), inserted)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Valid Delete by ID",
|
||||
id: inserted.ID.Hex(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalidObjectID",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
count, err := repo.Delete(context.Background(), tt.id)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count, "刪除應影響 1 條記錄")
|
||||
|
||||
// 確認已刪除
|
||||
_, err = repo.FindOne(context.Background(), tt.id)
|
||||
assert.ErrorIs(t, err, ErrNotFound, "記錄應該不存在")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomAutoIDModel_Inc(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
initialData *entity.AutoID
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Increment non-existing counter (upsert)",
|
||||
initialData: nil, // 不提供初始數據,預期會自動插入新數據
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Increment existing counter",
|
||||
initialData: &entity.AutoID{
|
||||
Name: "auto_id",
|
||||
Counter: 5,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestAutoIDRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
// 插入初始數據(如果有)
|
||||
if tt.initialData != nil {
|
||||
err := repo.Insert(context.Background(), tt.initialData)
|
||||
assert.NoError(t, err, "初始插入操作應該成功")
|
||||
}
|
||||
|
||||
// 創建一個 AutoID 結構來保存增量操作後的結果
|
||||
var result entity.AutoID
|
||||
|
||||
// 執行 Inc 方法
|
||||
err = repo.Inc(context.Background(), &result)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err, "應該返回錯誤")
|
||||
} else {
|
||||
assert.NoError(t, err, "Inc 操作應該成功")
|
||||
|
||||
// 驗證結果中的 Counter 值是否正確增量
|
||||
expectedCounter := uint64(1)
|
||||
if tt.initialData != nil {
|
||||
expectedCounter = tt.initialData.Counter + 1
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedCounter, result.Counter, "Counter 值應該自增")
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
)
|
||||
|
||||
// Common repository errors
|
||||
var (
|
||||
// ErrNotFound is returned when a requested resource is not found
|
||||
ErrNotFound = mon.ErrNotFound
|
||||
|
||||
// ErrInvalidObjectID is returned when an invalid MongoDB ObjectID is provided
|
||||
ErrInvalidObjectID = errors.New("invalid objectId")
|
||||
|
||||
// ErrDuplicateKey is returned when attempting to insert a document with a duplicate key
|
||||
ErrDuplicateKey = errors.New("duplicate key error")
|
||||
|
||||
// ErrInvalidInput is returned when input validation fails
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
const (
|
||||
Host = "127.0.0.1"
|
||||
Port = "27017"
|
||||
Schema = "mongodb"
|
||||
)
|
||||
|
||||
func startMongoContainer() (string, string, func(), error) {
|
||||
ctx := context.Background()
|
||||
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: "mongo:latest",
|
||||
ExposedPorts: []string{"27017/tcp"},
|
||||
WaitingFor: wait.ForListeningPort("27017/tcp"),
|
||||
}
|
||||
|
||||
mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
port, err := mongoC.MappedPort(ctx, Port)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
host, err := mongoC.Host(ctx)
|
||||
if err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("mongodb://%s:%s", host, port.Port())
|
||||
tearDown := func() {
|
||||
mongoC.Terminate(ctx)
|
||||
}
|
||||
|
||||
fmt.Printf("Connecting to %s\n", uri)
|
||||
|
||||
return host, port.Port(), tearDown, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,368 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
"backend/pkg/library/mongo"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
type UserRepositoryParam struct {
|
||||
Conf *mongo.Conf
|
||||
CacheConf cache.CacheConf
|
||||
DBOpts []mon.Option
|
||||
CacheOpts []cache.Option
|
||||
}
|
||||
|
||||
type UserRepository struct {
|
||||
DB mongo.DocumentDBWithCacheUseCase
|
||||
}
|
||||
|
||||
func NewUserRepository(param UserRepositoryParam) repository.UserRepository {
|
||||
e := entity.User{}
|
||||
documentDB, err := mongo.MustDocumentDBWithCache(
|
||||
param.Conf,
|
||||
e.CollectionName(),
|
||||
param.CacheConf,
|
||||
param.DBOpts,
|
||||
param.CacheOpts,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &UserRepository{
|
||||
DB: documentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *UserRepository) Insert(ctx context.Context, data *entity.User) error {
|
||||
if data.ID.IsZero() {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.ID = bson.NewObjectID()
|
||||
data.CreateAt = &now
|
||||
data.UpdateAt = &now
|
||||
}
|
||||
|
||||
rk := domain.GetUserRedisKey(data.ID.Hex())
|
||||
_, err := repo.DB.InsertOne(ctx, rk, data)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (repo *UserRepository) FindOne(ctx context.Context, id string) (*entity.User, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
var data entity.User
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
err = repo.DB.FindOne(ctx, rk, &data, bson.M{"_id": oid})
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *UserRepository) Update(ctx context.Context, data *entity.User) (*mongodriver.UpdateResult, error) {
|
||||
now := time.Now().UTC().UnixNano()
|
||||
data.UpdateAt = &now
|
||||
|
||||
rk := domain.GetUserRedisKey(data.ID.Hex())
|
||||
res, err := repo.DB.UpdateOne(ctx, rk, bson.M{"_id": data.ID}, bson.M{"$set": data})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (repo *UserRepository) Delete(ctx context.Context, id string) (int64, error) {
|
||||
oid, err := bson.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return 0, ErrInvalidObjectID
|
||||
}
|
||||
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
res, err := repo.DB.DeleteOne(ctx, rk, bson.M{"_id": oid})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (repo *UserRepository) UpdateUserDetailsByUID(ctx context.Context, data *repository.UpdateUserInfoRequest) error {
|
||||
updateFields := bson.M{}
|
||||
|
||||
if data.AlarmCategory != nil {
|
||||
updateFields["alarm_category"] = *data.AlarmCategory
|
||||
}
|
||||
if data.UserStatus != nil {
|
||||
updateFields["user_status"] = *data.UserStatus
|
||||
}
|
||||
if data.PreferredLanguage != nil {
|
||||
updateFields["preferred_language"] = *data.PreferredLanguage
|
||||
}
|
||||
if data.Currency != nil {
|
||||
updateFields["currency"] = *data.Currency
|
||||
}
|
||||
if data.Nickname != nil {
|
||||
updateFields["nickname"] = *data.Nickname
|
||||
}
|
||||
if data.AvatarURL != nil {
|
||||
updateFields["avatar_url"] = *data.AvatarURL
|
||||
}
|
||||
if data.FullName != nil {
|
||||
updateFields["full_name"] = *data.FullName
|
||||
}
|
||||
if data.GenderCode != nil {
|
||||
updateFields["gender_code"] = *data.GenderCode
|
||||
}
|
||||
if data.Birthdate != nil {
|
||||
updateFields["birthdate"] = *data.Birthdate
|
||||
}
|
||||
if data.Address != nil {
|
||||
updateFields["address"] = *data.Address
|
||||
}
|
||||
|
||||
if len(updateFields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
updateFields["update_at"] = time.Now().UTC().UnixNano()
|
||||
|
||||
filter := bson.M{"uid": data.UID}
|
||||
update := bson.M{"$set": updateFields}
|
||||
|
||||
// 不常寫,再找一次可接受
|
||||
id := repo.UIDToID(ctx, data.UID)
|
||||
if id == "" {
|
||||
return errors.New("invalid uid")
|
||||
}
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return ErrNotFound // 自定義的錯誤表示未找到記錄
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *UserRepository) UpdateStatus(ctx context.Context, uid string, status int32) error {
|
||||
filter := bson.M{"uid": uid}
|
||||
|
||||
// 構建更新內容,僅更新 status 字段並記錄 update_at 時間
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"user_status": status,
|
||||
"update_at": time.Now().UTC().UnixNano(),
|
||||
},
|
||||
}
|
||||
|
||||
// 不常寫,再找一次可接受
|
||||
id := repo.UIDToID(ctx, uid)
|
||||
if id == "" {
|
||||
return errors.New("invalid uid")
|
||||
}
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
|
||||
// 執行更新操作
|
||||
result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update status for uid %s: %w", uid, err)
|
||||
}
|
||||
|
||||
// 檢查更新結果,若沒有匹配的文檔,則返回錯誤
|
||||
if result.MatchedCount == 0 {
|
||||
return ErrNotFound // 自定義的錯誤表示未找到記錄
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *UserRepository) FindOneByUID(ctx context.Context, uid string) (*entity.User, error) {
|
||||
// 構建查找條件
|
||||
filter := bson.M{"uid": uid}
|
||||
var data entity.User
|
||||
|
||||
// 不常寫,再找一次可接受
|
||||
id := repo.UIDToID(ctx, uid)
|
||||
if id == "" {
|
||||
return nil, errors.New("invalid uid")
|
||||
}
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
|
||||
err := repo.DB.FindOne(ctx, rk, &data, filter)
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *UserRepository) FindOneByNickName(ctx context.Context, nickName string) (*entity.User, error) {
|
||||
// 構建查找條件
|
||||
filter := bson.M{"nickname": nickName}
|
||||
var data entity.User
|
||||
|
||||
err := repo.DB.GetClient().FindOne(ctx, &data, filter)
|
||||
switch {
|
||||
case err == nil:
|
||||
return &data, nil
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
return nil, ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *UserRepository) ListMembers(ctx context.Context, params *repository.UserQueryParams) ([]*entity.User, int64, error) {
|
||||
// 構建查詢條件
|
||||
filter := bson.M{}
|
||||
|
||||
if params.AlarmCategory != nil {
|
||||
filter["alarm_category"] = *params.AlarmCategory
|
||||
}
|
||||
if params.UserStatus != nil {
|
||||
filter["user_status"] = *params.UserStatus
|
||||
}
|
||||
if params.CreateStartTime != nil || params.CreateEndTime != nil {
|
||||
timeFilter := bson.M{}
|
||||
if params.CreateStartTime != nil {
|
||||
timeFilter["$gte"] = *params.CreateStartTime
|
||||
}
|
||||
if params.CreateEndTime != nil {
|
||||
timeFilter["$lte"] = *params.CreateEndTime
|
||||
}
|
||||
filter["create_at"] = timeFilter
|
||||
}
|
||||
|
||||
// 計算符合條件的總數
|
||||
count, err := repo.DB.GetClient().CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 構建查詢選項(分頁)
|
||||
opts := options.Find().
|
||||
SetSkip(params.PageSize * (params.PageIndex - 1)).
|
||||
SetLimit(params.PageSize)
|
||||
|
||||
// 執行查詢
|
||||
var users = make([]*entity.User, 0, params.PageSize)
|
||||
err = repo.DB.GetClient().Find(ctx, &users, filter, opts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return users, count, nil
|
||||
}
|
||||
|
||||
func (repo *UserRepository) UpdateEmailVerifyStatus(ctx context.Context, uid, email string) error {
|
||||
// 構建查找條件
|
||||
filter := bson.M{"uid": uid}
|
||||
|
||||
// 構建更新內容,僅更新 status 字段並記錄 update_at 時間
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"email": email,
|
||||
"update_at": time.Now().UTC().UnixNano(),
|
||||
},
|
||||
}
|
||||
|
||||
// 不常寫,再找一次可接受
|
||||
id := repo.UIDToID(ctx, uid)
|
||||
if id == "" {
|
||||
return errors.New("invalid uid")
|
||||
}
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
|
||||
// 執行更新操作
|
||||
result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update status for uid %s: %w", uid, err)
|
||||
}
|
||||
|
||||
// 檢查更新結果,若沒有匹配的文檔,則返回錯誤
|
||||
if result.MatchedCount == 0 {
|
||||
return ErrNotFound // 自定義的錯誤表示未找到記錄
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *UserRepository) UpdatePhoneVerifyStatus(ctx context.Context, uid, phone string) error {
|
||||
// 構建查找條件
|
||||
filter := bson.M{"uid": uid}
|
||||
|
||||
// 構建更新內容,僅更新 status 字段並記錄 update_at 時間
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"phone_number": phone,
|
||||
"update_at": time.Now().UTC().UnixNano(),
|
||||
},
|
||||
}
|
||||
|
||||
// 不常寫,再找一次可接受
|
||||
id := repo.UIDToID(ctx, uid)
|
||||
if id == "" {
|
||||
return errors.New("invalid uid")
|
||||
}
|
||||
rk := domain.GetUserRedisKey(id)
|
||||
|
||||
// 執行更新操作
|
||||
result, err := repo.DB.UpdateOne(ctx, rk, filter, update, &options.UpdateOneOptions{Upsert: &[]bool{false}[0]})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update status for uid %s: %w", uid, err)
|
||||
}
|
||||
|
||||
// 檢查更新結果,若沒有匹配的文檔,則返回錯誤
|
||||
if result.MatchedCount == 0 {
|
||||
return ErrNotFound // 自定義的錯誤表示未找到記錄
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *UserRepository) UIDToID(ctx context.Context, uid string) string {
|
||||
filter := bson.M{"uid": uid}
|
||||
var user entity.User
|
||||
opts := options.FindOne().SetProjection(bson.M{
|
||||
"_id": 1,
|
||||
})
|
||||
err := repo.DB.GetClient().FindOne(ctx, &user, filter, opts)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return user.ID.Hex()
|
||||
}
|
||||
|
||||
func (repo *UserRepository) Index20241226001UP(ctx context.Context) (*mongodriver.Cursor, error) {
|
||||
// db.user_info.createIndex({"uid": 1},{unique: true})
|
||||
repo.DB.PopulateIndex(ctx, "uid", 1, true)
|
||||
// db.user_info.createIndex({"create_at": 1})
|
||||
repo.DB.PopulateIndex(ctx, "create_at", 1, false)
|
||||
|
||||
return repo.DB.GetClient().Indexes().List(ctx)
|
||||
}
|
||||
|
|
@ -0,0 +1,387 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/repository"
|
||||
|
||||
mgo "backend/pkg/library/mongo"
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stores/cache"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func SetupTestUserRepository(db string) (repository.UserRepository, func(), error) {
|
||||
h, p, tearDown, err := startMongoContainer()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s, _ := miniredis.Run()
|
||||
|
||||
conf := &mgo.Conf{
|
||||
Schema: Schema,
|
||||
Host: fmt.Sprintf("%s:%s", h, p),
|
||||
Database: db,
|
||||
MaxStaleness: 300,
|
||||
MaxPoolSize: 100,
|
||||
MinPoolSize: 100,
|
||||
MaxConnIdleTime: 300,
|
||||
Compressors: []string{},
|
||||
EnableStandardReadWriteSplitMode: false,
|
||||
ConnectTimeoutMs: 3000,
|
||||
}
|
||||
|
||||
cacheConf := cache.CacheConf{
|
||||
cache.NodeConf{
|
||||
RedisConf: redis.RedisConf{
|
||||
Host: s.Addr(),
|
||||
Type: redis.NodeType,
|
||||
},
|
||||
Weight: 100,
|
||||
},
|
||||
}
|
||||
|
||||
cacheOpts := []cache.Option{
|
||||
cache.WithExpiry(1000 * time.Microsecond),
|
||||
cache.WithNotFoundExpiry(1000 * time.Microsecond),
|
||||
}
|
||||
|
||||
param := UserRepositoryParam{
|
||||
Conf: conf,
|
||||
CacheConf: cacheConf,
|
||||
CacheOpts: cacheOpts,
|
||||
}
|
||||
|
||||
repo := NewUserRepository(param)
|
||||
_, _ = repo.Index20241226001UP(context.Background())
|
||||
|
||||
return repo, tearDown, nil
|
||||
}
|
||||
|
||||
func TestCustomUserModel_UpdateEmailVerifyStatus(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid_email",
|
||||
Email: proto.String("old_email@example.com"),
|
||||
}
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = repo.UpdateEmailVerifyStatus(context.Background(), "test_uid_email", "new_email@example.com")
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedUser, err := repo.FindOneByUID(context.Background(), "test_uid_email")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "new_email@example.com", *updatedUser.Email)
|
||||
}
|
||||
|
||||
func TestCustomUserModel_UpdatePhoneVerifyStatus(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid_phone",
|
||||
PhoneNumber: proto.String("123456789"),
|
||||
}
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = repo.UpdatePhoneVerifyStatus(context.Background(), "test_uid_phone", "987654321")
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedUser, err := repo.FindOneByUID(context.Background(), "test_uid_phone")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "987654321", *updatedUser.PhoneNumber)
|
||||
}
|
||||
|
||||
func TestCustomUserModel_UpdateUserDetailsByUID(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
initialData *entity.User
|
||||
updateData *repository.UpdateUserInfoRequest
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Update user details by UID",
|
||||
initialData: &entity.User{
|
||||
UID: "test_uid_1",
|
||||
Nickname: proto.String("old_nickname"),
|
||||
FullName: proto.String("Old Full Name"),
|
||||
},
|
||||
updateData: &repository.UpdateUserInfoRequest{
|
||||
UID: "test_uid_1",
|
||||
Nickname: proto.String("new_nickname"),
|
||||
FullName: proto.String("New Full Name"),
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "User not found",
|
||||
initialData: nil,
|
||||
updateData: &repository.UpdateUserInfoRequest{
|
||||
UID: "non_existent_uid",
|
||||
Nickname: proto.String("nickname"),
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.initialData != nil {
|
||||
err := repo.Insert(context.Background(), tt.initialData)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
err := repo.UpdateUserDetailsByUID(context.Background(), tt.updateData)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedUser, err := repo.FindOneByUID(context.Background(), tt.updateData.UID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, *tt.updateData.Nickname, *updatedUser.Nickname)
|
||||
assert.Equal(t, *tt.updateData.FullName, *updatedUser.FullName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomUserModel_FindOneByUID(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid",
|
||||
}
|
||||
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, err := repo.FindOneByUID(context.Background(), "test_uid")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test_uid", user.UID)
|
||||
}
|
||||
|
||||
func TestCustomUserModel_ListMembers(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
users := []*entity.User{
|
||||
{UID: "uid1", UserStatus: member.AccountStatusActive},
|
||||
{UID: "uid2", UserStatus: member.AccountStatusActive},
|
||||
{UID: "uid3", UserStatus: member.AccountStatusUnverified},
|
||||
}
|
||||
|
||||
for _, user := range users {
|
||||
err := repo.Insert(context.Background(), user)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
activeStatus := member.AccountStatusActive
|
||||
params := &repository.UserQueryParams{
|
||||
UserStatus: &activeStatus,
|
||||
PageSize: 2,
|
||||
PageIndex: 1,
|
||||
}
|
||||
|
||||
result, count, err := repo.ListMembers(context.Background(), params)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count)
|
||||
assert.Len(t, result, 2)
|
||||
}
|
||||
|
||||
func TestCustomUserModel_FindOne(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
// 準備測試資料
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid",
|
||||
}
|
||||
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id string
|
||||
expectError bool
|
||||
expectedUID string
|
||||
}{
|
||||
{
|
||||
name: "Valid FindOne",
|
||||
id: initialUser.ID.Hex(),
|
||||
expectError: false,
|
||||
expectedUID: initialUser.UID,
|
||||
},
|
||||
{
|
||||
name: "Invalid ObjectID",
|
||||
id: "invalid_id",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Non-existent ObjectID",
|
||||
id: primitive.NewObjectID().Hex(),
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := repo.FindOne(context.Background(), tt.id)
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedUID, result.UID, "找到的 UID 應符合預期")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomUserModel_Update(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
// 準備測試資料
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid",
|
||||
}
|
||||
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
updatedUID := "test_uid"
|
||||
initialUser.UID = updatedUID
|
||||
|
||||
// 執行更新操作
|
||||
_, err = repo.Update(context.Background(), initialUser)
|
||||
assert.NoError(t, err, "應成功更新資料")
|
||||
|
||||
// 確認更新結果
|
||||
updatedAccountUid, err := repo.FindOne(context.Background(), initialUser.ID.Hex())
|
||||
assert.NoError(t, err, "應能找到更新後的資料")
|
||||
assert.Equal(t, updatedUID, updatedAccountUid.UID, "更新後的 UID 應符合預期")
|
||||
}
|
||||
|
||||
func TestCustomUserModel_Delete(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
// 準備測試資料
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid",
|
||||
}
|
||||
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
// 執行刪除操作
|
||||
count, err := repo.Delete(context.Background(), initialUser.ID.Hex())
|
||||
assert.NoError(t, err, "應成功刪除資料")
|
||||
assert.Equal(t, int64(1), count, "刪除數量應為 1")
|
||||
|
||||
// 確認資料已被刪除
|
||||
_, err = repo.FindOne(context.Background(), initialUser.ID.Hex())
|
||||
assert.Error(t, err, "應無法找到已刪除的資料")
|
||||
assert.Equal(t, ErrNotFound, err, "應返回 ErrNotFound 錯誤")
|
||||
}
|
||||
|
||||
func TestUserRepository_UpdateStatus(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
// 插入測試數據
|
||||
e := &entity.User{
|
||||
UID: "test_uid",
|
||||
UserStatus: member.AccountStatusActive,
|
||||
}
|
||||
err = repo.Insert(context.TODO(), e)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 測試用例
|
||||
tests := []struct {
|
||||
name string
|
||||
uid string
|
||||
status member.Status
|
||||
expectError bool
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Valid UID - Successful update",
|
||||
uid: e.UID,
|
||||
status: member.AccountStatusActive,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid UID - No mapping to ID",
|
||||
uid: "invalid_uid",
|
||||
status: 3,
|
||||
expectError: true,
|
||||
expectedErr: errors.New("invalid uid"),
|
||||
},
|
||||
{
|
||||
name: "Non-existent UID",
|
||||
uid: "non_existent_uid",
|
||||
status: 4,
|
||||
expectError: true,
|
||||
expectedErr: errors.New("invalid uid"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := repo.UpdateStatus(context.TODO(), tt.uid, tt.status.ToInt32())
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, tt.expectedErr.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 驗證數據是否正確更新
|
||||
user, err := repo.FindOneByUID(context.TODO(), tt.uid)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.status, user.UserStatus, "UserStatus 應更新為預期值")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomUserModel_FindOneByNickName(t *testing.T) {
|
||||
repo, tearDown, err := SetupTestUserRepository("testDB")
|
||||
defer tearDown()
|
||||
assert.NoError(t, err)
|
||||
|
||||
initialUser := &entity.User{
|
||||
UID: "test_uid",
|
||||
Nickname: proto.String("my_name"),
|
||||
}
|
||||
|
||||
err = repo.Insert(context.Background(), initialUser)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user, err := repo.FindOneByNickName(context.Background(), "my_name")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test_uid", user.UID)
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/repository"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
type VerifyCodeRepository struct {
|
||||
redis *redis.Redis
|
||||
}
|
||||
|
||||
func NewVerifyCodeRepository(r *redis.Redis) repository.VerifyCodeRepository {
|
||||
return &VerifyCodeRepository{
|
||||
redis: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (repo *VerifyCodeRepository) IsVerifyCodeExist(ctx context.Context, loginID, checkType string) (string, error) {
|
||||
rk := domain.GetCheckVerifyKey(checkType, loginID)
|
||||
// Retrieve the stored code from Redis
|
||||
storedCode, err := repo.redis.GetCtx(ctx, rk)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if the code exists in Redis
|
||||
if storedCode == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return storedCode, nil
|
||||
}
|
||||
|
||||
func (repo *VerifyCodeRepository) SetVerifyCode(ctx context.Context, loginID, checkType, code string) error {
|
||||
rk := domain.GetCheckVerifyKey(checkType, loginID)
|
||||
err := repo.redis.SetexCtx(ctx, rk, code, 600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *VerifyCodeRepository) DelVerifyCode(ctx context.Context, loginID, checkType string) error {
|
||||
rk := domain.GetCheckVerifyKey(checkType, loginID)
|
||||
_, err := repo.redis.DelCtx(ctx, rk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stores/redis"
|
||||
)
|
||||
|
||||
func setupMiniRedis() (*miniredis.Miniredis, *redis.Redis) {
|
||||
// 啟動 setupMiniRedis 作為模擬的 Redis 服務
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
panic("failed to start miniRedis: " + err.Error())
|
||||
}
|
||||
|
||||
// 使用 setupMiniRedis 的地址配置 go-zero Redis 客戶端
|
||||
redisConf := redis.RedisConf{
|
||||
Host: mr.Addr(),
|
||||
Type: "node",
|
||||
}
|
||||
r := redis.MustNewRedis(redisConf)
|
||||
|
||||
return mr, r
|
||||
}
|
||||
|
||||
func TestVerifyCodeRepository_IsVerifyCodeExist(t *testing.T) {
|
||||
mr, r := setupMiniRedis()
|
||||
defer mr.Close()
|
||||
|
||||
repo := NewVerifyCodeRepository(r)
|
||||
ctx := context.Background()
|
||||
|
||||
loginID := "test_user"
|
||||
checkType := "email"
|
||||
code := "123456"
|
||||
|
||||
// 預設驗證碼不存在
|
||||
rk := domain.GetCheckVerifyKey(checkType, loginID)
|
||||
existingCode, err := repo.IsVerifyCodeExist(ctx, loginID, checkType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", existingCode, "驗證碼應該不存在")
|
||||
|
||||
// 設置驗證碼
|
||||
err = r.SetexCtx(ctx, rk, code, 600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 再次檢查驗證碼
|
||||
existingCode, err = repo.IsVerifyCodeExist(ctx, loginID, checkType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, code, existingCode, "驗證碼應該正確存在")
|
||||
}
|
||||
|
||||
func TestVerifyCodeRepository_DelVerifyCode(t *testing.T) {
|
||||
mr, r := setupMiniRedis()
|
||||
defer mr.Close()
|
||||
|
||||
repo := NewVerifyCodeRepository(r)
|
||||
ctx := context.Background()
|
||||
|
||||
loginID := "test_user"
|
||||
checkType := "email"
|
||||
code := "123456"
|
||||
|
||||
// 設置驗證碼
|
||||
rk := domain.GetCheckVerifyKey(checkType, loginID)
|
||||
err := r.SetexCtx(ctx, rk, code, 600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 刪除驗證碼
|
||||
err = repo.DelVerifyCode(ctx, loginID, checkType)
|
||||
assert.NoError(t, err, "刪除驗證碼應該成功")
|
||||
|
||||
// 確認驗證碼已刪除
|
||||
existingCode, err := r.GetCtx(ctx, rk)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", existingCode, "驗證碼應該已刪除")
|
||||
}
|
||||
|
||||
func TestVerifyCodeRepository_SetVerifyCode(t *testing.T) {
|
||||
mr, r := setupMiniRedis()
|
||||
defer mr.Close()
|
||||
|
||||
repo := NewVerifyCodeRepository(r)
|
||||
ctx := context.Background()
|
||||
|
||||
// 測試參數
|
||||
loginID := "test_user"
|
||||
checkType := "email"
|
||||
code := "123456"
|
||||
|
||||
// 測試 SetVerifyCode 方法
|
||||
err := repo.SetVerifyCode(ctx, loginID, checkType, code)
|
||||
assert.NoError(t, err, "設置驗證碼應該成功")
|
||||
|
||||
// 驗證數據是否正確存儲到 Redis
|
||||
exist, err := repo.IsVerifyCodeExist(ctx, loginID, checkType)
|
||||
assert.NoError(t, err, "設置驗證碼應該成功")
|
||||
assert.Equal(t, code, exist)
|
||||
|
||||
rk := domain.GetCheckVerifyKey(checkType, loginID)
|
||||
// 測試驗證碼是否正確過期
|
||||
mr.FastForward(800) // 模擬 600 秒後
|
||||
storedCode, err := r.GetCtx(ctx, rk)
|
||||
assert.NoError(t, err, "過期後檢查 Redis 不應報錯")
|
||||
fmt.Println(storedCode)
|
||||
//assert.Equal(t, "", storedCode, "過期後驗證碼應該被清除")
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"backend/pkg/member/domain/config"
|
||||
"backend/pkg/member/domain/repository"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
)
|
||||
|
||||
type MemberUseCaseParam struct {
|
||||
Account repository.AccountRepository
|
||||
User repository.UserRepository
|
||||
AccountUID repository.AccountUIDRepository
|
||||
VerifyCodeModel repository.VerifyCodeRepository
|
||||
GenerateUID repository.AutoIDRepository
|
||||
Config config.Config
|
||||
}
|
||||
|
||||
type MemberUseCase struct {
|
||||
MemberUseCaseParam
|
||||
}
|
||||
|
||||
func MustMemberUseCase(param MemberUseCaseParam) usecase.AccountUseCase {
|
||||
return &MemberUseCase{
|
||||
param,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
)
|
||||
|
||||
func (use *MemberUseCase) BindUserInfo(ctx context.Context, req usecase.CreateUserInfoRequest) error {
|
||||
// prepare 準備資料
|
||||
// 準備資料
|
||||
insert := &entity.User{
|
||||
UID: req.UID,
|
||||
AvatarURL: req.AvatarURL,
|
||||
FullName: req.FullName,
|
||||
Nickname: req.Nickname,
|
||||
GenderCode: req.GenderCode,
|
||||
Birthdate: req.Birthdate,
|
||||
Address: req.Address,
|
||||
AlarmCategory: req.AlarmCategory,
|
||||
UserStatus: req.UserStatus,
|
||||
PreferredLanguage: req.PreferredLanguage,
|
||||
Currency: req.Currency,
|
||||
}
|
||||
|
||||
// Insert 新增
|
||||
if err := use.User.Insert(ctx, insert); err != nil {
|
||||
e := errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.BindingUserTabletErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "User.Insert"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to binding user info").Wrap(err)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) BindAccount(ctx context.Context, req usecase.BindingUser) (usecase.BindingUser, error) {
|
||||
// 先確定有這個Account
|
||||
_, err := use.Account.FindOneByAccount(ctx, req.LoginID)
|
||||
if err != nil {
|
||||
var e *errs.LibError
|
||||
switch {
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
e = errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToFindAccountErrorCode,
|
||||
fmt.Sprintf("failed to insert account: %s", req.UID),
|
||||
)
|
||||
default:
|
||||
e = errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToFindAccountErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "User.FindOneByAccount"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to find account").Wrap(err)
|
||||
}
|
||||
|
||||
return usecase.BindingUser{}, e
|
||||
}
|
||||
|
||||
uid := req.UID
|
||||
// 有帳號,沒UID 表示他是要產生一個新的UID,產一個給他
|
||||
if req.UID == "" {
|
||||
uid, err = use.Generate(ctx)
|
||||
if err != nil {
|
||||
// generate 裡面會產生 error
|
||||
// usecase 印出錯誤其中一個準則,在最基底的 uc 裡面印錯誤
|
||||
return usecase.BindingUser{}, err
|
||||
}
|
||||
}
|
||||
if err := use.AccountUID.Insert(ctx, &entity.AccountUID{
|
||||
LoginID: req.LoginID,
|
||||
UID: uid,
|
||||
Type: req.Type,
|
||||
}); err != nil {
|
||||
e := errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToBindAccountErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "User.Insert"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to bind account").Wrap(err)
|
||||
|
||||
return usecase.BindingUser{}, e
|
||||
}
|
||||
|
||||
return usecase.BindingUser{
|
||||
LoginID: req.LoginID,
|
||||
UID: uid,
|
||||
Type: req.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) BindVerifyEmail(ctx context.Context, uid, email string) error {
|
||||
err := use.User.UpdateEmailVerifyStatus(ctx, uid, email)
|
||||
if err != nil {
|
||||
e := errs.DatabaseErrorWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToFindAccountErrorCode,
|
||||
fmt.Sprintf("failed to Binding uid: %s, email: %s", uid, email),
|
||||
)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) BindVerifyPhone(ctx context.Context, uid, phone string) error {
|
||||
err := use.User.UpdatePhoneVerifyStatus(ctx, uid, phone)
|
||||
if err != nil {
|
||||
e := errs.DatabaseErrorWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToFindAccountErrorCode,
|
||||
fmt.Sprintf("failed to Binding uid: %s, phone: %s", uid, phone),
|
||||
)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
mockRepo "backend/pkg/member/mock/repository"
|
||||
)
|
||||
|
||||
func TestMemberUseCase_BindUserInfo(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepository := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepository,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.CreateUserInfoRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
req: usecase.CreateUserInfoRequest{
|
||||
UID: "test-uid",
|
||||
AvatarURL: proto.String("http://example.com/avatar.png"),
|
||||
FullName: proto.String("Test User"),
|
||||
Nickname: proto.String("Tester"),
|
||||
GenderCode: proto.Int64(1),
|
||||
UserStatus: 1,
|
||||
PreferredLanguage: "en",
|
||||
Currency: "USD",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "failed to bind user info due to insert error",
|
||||
req: usecase.CreateUserInfoRequest{
|
||||
UID: "test-uid",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("database error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
err := uc.BindUserInfo(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "failed to binding user info")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberUseCase_BindAccount(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAccountRepository := mockRepo.NewMockAccountRepository(mockCtrl)
|
||||
mockAccountUIDRepository := mockRepo.NewMockAccountUIDRepository(mockCtrl)
|
||||
mockAutoIDRepository := mockRepo.NewMockAutoIDRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
Account: mockAccountRepository,
|
||||
AccountUID: mockAccountUIDRepository,
|
||||
GenerateUID: mockAutoIDRepository,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.BindingUser
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
req: usecase.BindingUser{
|
||||
LoginID: "testLoginID",
|
||||
UID: "testUID",
|
||||
Type: member.AccountTypeMail,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(&entity.Account{}, nil)
|
||||
mockAccountUIDRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "successful bind with generated UID",
|
||||
req: usecase.BindingUser{
|
||||
LoginID: "testLoginID",
|
||||
Type: member.AccountTypeMail,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(&entity.Account{}, nil)
|
||||
//uc.EXPECT().Generate(gomock.Any()).Return("generatedUID", nil)
|
||||
mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).Return(nil)
|
||||
mockAutoIDRepository.EXPECT().GetUIDFromNum(gomock.Any()).Return("DOOOOOOD", nil)
|
||||
mockAccountUIDRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "failed to find account",
|
||||
req: usecase.BindingUser{
|
||||
LoginID: "testLoginID",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(nil, errors.New("not found"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "failed to insert account UID",
|
||||
req: usecase.BindingUser{
|
||||
LoginID: "testLoginID",
|
||||
UID: "testUID",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepository.EXPECT().FindOneByAccount(gomock.Any(), "testLoginID").Return(&entity.Account{}, nil)
|
||||
mockAccountUIDRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("insert error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
result, err := uc.BindAccount(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.req.LoginID, result.LoginID)
|
||||
assert.Equal(t, tt.req.Type, result.Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberUseCase_BindVerifyEmail(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepository := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepository,
|
||||
})
|
||||
tests := []struct {
|
||||
name string
|
||||
uid string
|
||||
email string
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful email verification",
|
||||
uid: "testUID",
|
||||
email: "test@example.com",
|
||||
mockSetup: func() {
|
||||
mockUserRepository.EXPECT().UpdateEmailVerifyStatus(gomock.Any(), "testUID", "test@example.com").Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "failed email verification",
|
||||
uid: "testUID",
|
||||
email: "test@example.com",
|
||||
mockSetup: func() {
|
||||
mockUserRepository.EXPECT().UpdateEmailVerifyStatus(gomock.Any(), "testUID", "test@example.com").Return(errors.New("update error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
err := uc.BindVerifyEmail(context.Background(), tt.uid, tt.email)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberUseCase_BindVerifyPhone(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepository := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepository,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
uid string
|
||||
phone string
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "successful phone verification",
|
||||
uid: "testUID",
|
||||
phone: "1234567890",
|
||||
mockSetup: func() {
|
||||
mockUserRepository.EXPECT().UpdatePhoneVerifyStatus(gomock.Any(), "testUID", "1234567890").Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "failed phone verification",
|
||||
uid: "testUID",
|
||||
phone: "1234567890",
|
||||
mockSetup: func() {
|
||||
mockUserRepository.EXPECT().UpdatePhoneVerifyStatus(gomock.Any(), "testUID", "1234567890").Return(errors.New("update error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
err := uc.BindVerifyPhone(context.Background(), tt.uid, tt.phone)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/entity"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
GIDLib "code.30cm.net/digimon/library-go/utils/invited_code"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
func (use *MemberUseCase) Generate(ctx context.Context) (string, error) {
|
||||
var data entity.AutoID
|
||||
err := use.GenerateUID.Inc(ctx, &data)
|
||||
if err != nil {
|
||||
e := errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToIncAccountErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "func", Value: "AutoIDModel.Inc"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to inc account num").Wrap(err)
|
||||
|
||||
return "", e
|
||||
}
|
||||
|
||||
// 使用 uint64 處理,避免溢出
|
||||
sum := GIDLib.InitAutoID + data.Counter
|
||||
if sum > math.MaxInt64 {
|
||||
return "",
|
||||
errs.InvalidRangeWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToIncAccountErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "func", Value: "MemberUseCase.Generate"},
|
||||
},
|
||||
"sum exceeds the maximum int64 value")
|
||||
}
|
||||
|
||||
uid, err := use.GenerateUID.GetUIDFromNum(int64(sum))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return uid, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
mockRepo "backend/pkg/member/mock/repository"
|
||||
|
||||
GIDLib "code.30cm.net/digimon/library-go/utils/invited_code"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestMemberUseCase_Generate(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAutoIDRepository := mockRepo.NewMockAutoIDRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
GenerateUID: mockAutoIDRepository,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "successful UID generation",
|
||||
mockSetup: func() {
|
||||
mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(ctx context.Context, data *entity.AutoID) error {
|
||||
data.Counter = 123
|
||||
return nil
|
||||
},
|
||||
)
|
||||
mockAutoIDRepository.EXPECT().GetUIDFromNum(int64(GIDLib.InitAutoID+123)).Return("generatedUID", nil)
|
||||
},
|
||||
wantErr: false,
|
||||
expected: "generatedUID",
|
||||
},
|
||||
{
|
||||
name: "increment error",
|
||||
mockSetup: func() {
|
||||
mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).Return(errors.New("increment error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "UID generation error",
|
||||
mockSetup: func() {
|
||||
mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(ctx context.Context, data *entity.AutoID) error {
|
||||
data.Counter = 123
|
||||
return nil
|
||||
},
|
||||
)
|
||||
mockAutoIDRepository.EXPECT().GetUIDFromNum(int64(GIDLib.InitAutoID+123)).Return("", errors.New("UID generation error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "sum exceeds int64",
|
||||
mockSetup: func() {
|
||||
mockAutoIDRepository.EXPECT().Inc(gomock.Any(), gomock.Any()).DoAndReturn(
|
||||
func(ctx context.Context, data *entity.AutoID) error {
|
||||
data.Counter = math.MaxInt64 // Force overflow
|
||||
return nil
|
||||
},
|
||||
)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
result, err := uc.Generate(context.Background())
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, result)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// ErrInvalidDigits is returned when the number of digits is invalid
|
||||
var ErrInvalidDigits = errors.New("invalid number of digits")
|
||||
|
||||
// generateVerifyCode generates a cryptographically secure verification code.
|
||||
// The digits parameter specifies the number of digits in the code (4-10).
|
||||
// If digits is 0 or negative, it defaults to 6 digits.
|
||||
func generateVerifyCode(digits int) (string, error) {
|
||||
// Default to 6 digits if not specified or invalid
|
||||
if digits <= 0 {
|
||||
digits = 6
|
||||
}
|
||||
|
||||
// Validate digit range
|
||||
if digits < 4 || digits > 10 {
|
||||
return "", fmt.Errorf("%w: digits must be between 4 and 10, got %d", ErrInvalidDigits, digits)
|
||||
}
|
||||
|
||||
// Calculate maximum value (10^digits - 1)
|
||||
exp := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(digits)), nil)
|
||||
|
||||
// Generate cryptographically secure random number
|
||||
randomNumber, err := rand.Int(rand.Reader, exp)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random number: %w", err)
|
||||
}
|
||||
|
||||
// Convert to string with zero padding
|
||||
verifyCode := fmt.Sprintf("%0*d", digits, randomNumber)
|
||||
|
||||
return verifyCode, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGenerateVerifyCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
digits int
|
||||
expectErr bool
|
||||
expectedLen int
|
||||
}{
|
||||
{
|
||||
name: "Generate 6-digit code (default)",
|
||||
digits: 0, // 測試預設值
|
||||
expectErr: false,
|
||||
expectedLen: 6,
|
||||
},
|
||||
{
|
||||
name: "Generate 4-digit code",
|
||||
digits: 4,
|
||||
expectErr: false,
|
||||
expectedLen: 4,
|
||||
},
|
||||
{
|
||||
name: "Generate 8-digit code",
|
||||
digits: 8,
|
||||
expectErr: false,
|
||||
expectedLen: 8,
|
||||
},
|
||||
{
|
||||
name: "Invalid digits (negative value)",
|
||||
digits: -3, // 測試無效位數
|
||||
expectErr: false,
|
||||
expectedLen: 6, // 預設值為6位數
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
code, err := generateVerifyCode(tt.digits)
|
||||
|
||||
// 驗證錯誤是否符合預期
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, code)
|
||||
|
||||
// 驗證生成的代碼長度是否符合預期
|
||||
assert.Equal(t, tt.expectedLen, len(code))
|
||||
|
||||
// 驗證代碼是否為純數字
|
||||
for _, c := range code {
|
||||
assert.True(t, c >= '0' && c <= '9', "Verify code should only contain digits")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,439 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/repository"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
)
|
||||
|
||||
// HasPasswordFunc is a function variable for password hashing, used for testing
|
||||
var HasPasswordFunc = HashPassword
|
||||
|
||||
// CreateUserAccount creates a new user account with the provided credentials.
|
||||
// It handles password hashing for Digimon platform accounts and stores the account data.
|
||||
func (use *MemberUseCase) CreateUserAccount(ctx context.Context, req usecase.CreateLoginUserRequest) error {
|
||||
// Validate input
|
||||
if err := use.validateCreateUserAccountRequest(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Process password for Digimon platform
|
||||
token, err := use.processPasswordForPlatform(req.Platform, req.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create account entity
|
||||
account := &entity.Account{
|
||||
LoginID: req.LoginID,
|
||||
Token: token,
|
||||
Platform: req.Platform,
|
||||
}
|
||||
|
||||
// Insert account into database
|
||||
return use.insertAccount(ctx, account, req)
|
||||
}
|
||||
|
||||
// validateCreateUserAccountRequest validates the create user account request
|
||||
func (use *MemberUseCase) validateCreateUserAccountRequest(req usecase.CreateLoginUserRequest) error {
|
||||
if req.LoginID == "" {
|
||||
return errs.InvalidFormatWithScope(code.CloudEPMember, "login ID is required")
|
||||
}
|
||||
|
||||
if req.Platform == member.Digimon && req.Token == "" {
|
||||
return errs.InvalidFormatWithScope(code.CloudEPMember, "password is required for Digimon platform")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPasswordForPlatform processes the password based on the platform type
|
||||
func (use *MemberUseCase) processPasswordForPlatform(platform member.Platform, password string) (string, error) {
|
||||
if platform != member.Digimon {
|
||||
return "", nil // No password processing for non-Digimon platforms
|
||||
}
|
||||
|
||||
// Hash password for Digimon platform
|
||||
token, err := HasPasswordFunc(password, use.Config.Bcrypt.Cost)
|
||||
if err != nil {
|
||||
return "", errs.NewError(
|
||||
code.CloudEPMember,
|
||||
code.CatSystem,
|
||||
domain.HashPasswordErrorCode,
|
||||
fmt.Sprintf("failed to encrypt password: %s", err.Error()),
|
||||
)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// insertAccount inserts the account into the database
|
||||
func (use *MemberUseCase) insertAccount(ctx context.Context, account *entity.Account, req usecase.CreateLoginUserRequest) error {
|
||||
err := use.Account.Insert(ctx, account)
|
||||
if err != nil {
|
||||
return errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.InsertAccountErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "Account.Insert"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"account duplicate").Wrap(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) GetUIDByAccount(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetUIDByAccountResponse, error) {
|
||||
account, err := use.AccountUID.FindUIDByLoginID(ctx, req.Account)
|
||||
if err != nil {
|
||||
var e *errs.LibError
|
||||
switch {
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
e = errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedFindUIDByLoginIDErrorCode,
|
||||
fmt.Sprintf("failed to find uid by account: %s", req.Account),
|
||||
)
|
||||
default:
|
||||
// 錯誤代碼 20-201-07
|
||||
e = errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedFindUIDByLoginIDErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "AccountUID.FindUIDByLoginID"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to find uid by account").Wrap(err)
|
||||
}
|
||||
|
||||
return usecase.GetUIDByAccountResponse{}, e
|
||||
}
|
||||
|
||||
return usecase.GetUIDByAccountResponse{
|
||||
UID: account.UID,
|
||||
Account: req.Account,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) GetUserAccountInfo(ctx context.Context, req usecase.GetUIDByAccountRequest) (usecase.GetAccountInfoResponse, error) {
|
||||
account, err := use.Account.FindOneByAccount(ctx, req.Account)
|
||||
if err != nil {
|
||||
var e *errs.LibError
|
||||
switch {
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
// 錯誤代碼 20-301-08
|
||||
e = errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedFindOneByAccountErrorCode,
|
||||
fmt.Sprintf("failed to find account: %s", req.Account),
|
||||
)
|
||||
default:
|
||||
// 錯誤代碼 20-201-08
|
||||
e = errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedFindOneByAccountErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "Account.FindOneByAccount"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to find account").Wrap(err)
|
||||
}
|
||||
|
||||
return usecase.GetAccountInfoResponse{}, e
|
||||
}
|
||||
|
||||
return usecase.GetAccountInfoResponse{
|
||||
Data: usecase.CreateLoginUserRequest{
|
||||
LoginID: account.LoginID,
|
||||
Platform: account.Platform,
|
||||
Token: account.Token,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ===========================
|
||||
|
||||
func (use *MemberUseCase) GetUserInfo(ctx context.Context, req usecase.GetUserInfoRequest) (usecase.UserInfo, error) {
|
||||
var user *entity.User
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case req.UID != "":
|
||||
user, err = use.User.FindOneByUID(ctx, req.UID)
|
||||
case req.NickName != "":
|
||||
user, err = use.User.FindOneByNickName(ctx, req.NickName)
|
||||
default:
|
||||
// 驗證至少提供一個查詢參數
|
||||
return usecase.UserInfo{}, errs.InvalidFormatWithScope(
|
||||
code.CloudEPMember,
|
||||
"UID or NickName must be provided",
|
||||
)
|
||||
}
|
||||
// 查詢失敗時處理錯誤
|
||||
if err != nil {
|
||||
return usecase.UserInfo{}, handleUserQueryError(ctx, err, req)
|
||||
}
|
||||
|
||||
// 返回查詢結果
|
||||
return mapUserEntityToUserInfo(user), nil
|
||||
}
|
||||
|
||||
// 將查詢錯誤處理邏輯封裝為單獨的函數
|
||||
func handleUserQueryError(ctx context.Context, err error, req usecase.GetUserInfoRequest) error {
|
||||
if errors.Is(err, mon.ErrNotFound) {
|
||||
return errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetUserInfoErrorCode,
|
||||
fmt.Sprintf("user not found: %s", req.UID),
|
||||
)
|
||||
}
|
||||
|
||||
return errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetUserInfoErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "MemberUseCase.GetUserInfo"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to query user info").Wrap(err)
|
||||
}
|
||||
|
||||
// 將用戶實體轉換為業務層數據結構
|
||||
func mapUserEntityToUserInfo(user *entity.User) usecase.UserInfo {
|
||||
return usecase.UserInfo{
|
||||
CreateUserInfoRequest: usecase.CreateUserInfoRequest{
|
||||
UID: user.UID,
|
||||
AlarmCategory: user.AlarmCategory,
|
||||
UserStatus: user.UserStatus,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Currency: user.Currency,
|
||||
Nickname: user.Nickname,
|
||||
AvatarURL: user.AvatarURL,
|
||||
FullName: user.FullName,
|
||||
GenderCode: user.GenderCode,
|
||||
Birthdate: user.Birthdate,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
Address: user.Address,
|
||||
Email: user.Email,
|
||||
},
|
||||
CreateTime: GetOriginalInt64(user.CreateAt),
|
||||
UpdateTime: GetOriginalInt64(user.UpdateAt),
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
|
||||
func (use *MemberUseCase) UpdateUserToken(ctx context.Context, req usecase.UpdateTokenRequest) error {
|
||||
// 密碼加密
|
||||
token, e := HasPasswordFunc(req.Token, use.Config.Bcrypt.Cost)
|
||||
if e != nil {
|
||||
return errs.NewError(
|
||||
code.CloudEPMember,
|
||||
code.CatSystem,
|
||||
domain.HashPasswordErrorCode,
|
||||
fmt.Sprintf("failed to encrypt err: %s", e.Error()),
|
||||
)
|
||||
}
|
||||
toInt8, err := safeInt64ToInt8(req.Platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = use.Account.UpdateTokenByLoginID(ctx, req.Account, token, member.Platform(toInt8))
|
||||
if err != nil {
|
||||
var e *errs.LibError
|
||||
switch {
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
// 錯誤代碼 20-301-08
|
||||
e = errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToUpdatePasswordErrorCode,
|
||||
fmt.Sprintf("failed to upadte password since account not found: %s", req.Account),
|
||||
)
|
||||
default:
|
||||
// 錯誤代碼 20-201-02
|
||||
e = errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToUpdatePasswordErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "Account.UpdateTokenByLoginID"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to update password").Wrap(err)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) UpdateUserInfo(ctx context.Context, req *usecase.UpdateUserInfoRequest) error {
|
||||
err := use.User.UpdateUserDetailsByUID(ctx, &repository.UpdateUserInfoRequest{
|
||||
UID: req.UID,
|
||||
AvatarURL: req.AvatarURL,
|
||||
FullName: req.FullName,
|
||||
Nickname: req.Nickname,
|
||||
GenderCode: req.GenderCode,
|
||||
Birthdate: req.Birthdate,
|
||||
Address: req.Address,
|
||||
AlarmCategory: req.AlarmCategory,
|
||||
UserStatus: req.UserStatus,
|
||||
PreferredLanguage: req.PreferredLanguage,
|
||||
Currency: req.Currency,
|
||||
})
|
||||
if err != nil {
|
||||
var e *errs.LibError
|
||||
switch {
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
e = errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToUpdateUserErrorCode,
|
||||
fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID),
|
||||
)
|
||||
default:
|
||||
e = errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToUpdateUserErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "User.UpdateUserDetailsByUid"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to update user info").Wrap(err)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) UpdateStatus(ctx context.Context, req usecase.UpdateStatusRequest) error {
|
||||
err := use.User.UpdateStatus(ctx, req.UID, req.Status.ToInt32())
|
||||
if err != nil {
|
||||
var e *errs.LibError
|
||||
switch {
|
||||
case errors.Is(err, mon.ErrNotFound):
|
||||
e = errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToFindUserErrorCode,
|
||||
fmt.Sprintf("failed to upadte use info since account not found: %s", req.UID),
|
||||
)
|
||||
|
||||
default:
|
||||
e = errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToUpdateUserStatusErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "User.UpdateStatus"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to update user info").Wrap(err)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) ListMember(ctx context.Context, req usecase.ListUserInfoRequest) (usecase.ListUserInfoResponse, error) {
|
||||
listMembers, total, err := use.User.ListMembers(ctx, &repository.UserQueryParams{
|
||||
AlarmCategory: req.AlarmCategory,
|
||||
UserStatus: req.UserStatus,
|
||||
CreateStartTime: req.CreateStartTime,
|
||||
CreateEndTime: req.CreateEndTime,
|
||||
PageSize: req.PageSize,
|
||||
PageIndex: req.PageIndex,
|
||||
})
|
||||
if err != nil {
|
||||
e := errs.DatabaseErrorWithScopeL(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetUserInfoErrorCode,
|
||||
logx.WithContext(ctx),
|
||||
[]logx.LogField{
|
||||
{Key: "req", Value: req},
|
||||
{Key: "func", Value: "User.ListMembers"},
|
||||
{Key: "err", Value: err.Error()},
|
||||
},
|
||||
"failed to list members").Wrap(err)
|
||||
|
||||
return usecase.ListUserInfoResponse{}, e
|
||||
}
|
||||
|
||||
var data = make([]usecase.UserInfo, 0, len(listMembers))
|
||||
|
||||
for _, item := range listMembers {
|
||||
data = append(data, usecase.UserInfo{
|
||||
CreateUserInfoRequest: usecase.CreateUserInfoRequest{
|
||||
UID: item.UID,
|
||||
AlarmCategory: item.AlarmCategory,
|
||||
UserStatus: item.UserStatus,
|
||||
PreferredLanguage: item.PreferredLanguage,
|
||||
Currency: item.Currency,
|
||||
Nickname: item.Nickname,
|
||||
AvatarURL: item.AvatarURL, // 按照先前的命名 AvatarURL
|
||||
FullName: item.FullName,
|
||||
GenderCode: item.GenderCode,
|
||||
Birthdate: item.Birthdate,
|
||||
PhoneNumber: item.PhoneNumber,
|
||||
Address: item.Address,
|
||||
Email: item.Email,
|
||||
},
|
||||
CreateTime: GetOriginalInt64(item.CreateAt), // 使用自定義指標轉換函數
|
||||
UpdateTime: GetOriginalInt64(item.UpdateAt),
|
||||
})
|
||||
}
|
||||
|
||||
return usecase.ListUserInfoResponse{
|
||||
Data: data,
|
||||
Page: usecase.Pager{
|
||||
Total: total,
|
||||
Index: req.PageIndex,
|
||||
Size: req.PageSize,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetOriginalInt64 取得原始值的函數
|
||||
func GetOriginalInt64(value *int64) int64 {
|
||||
if value == nil {
|
||||
return 0 // 處理 nil 的情況
|
||||
}
|
||||
|
||||
return *value
|
||||
}
|
||||
|
||||
func safeInt64ToInt8(n int64) (int8, error) {
|
||||
if n < math.MinInt8 || n > math.MaxInt8 {
|
||||
return 0, fmt.Errorf("int64 value %d out of int8 range", n)
|
||||
}
|
||||
|
||||
return int8(n), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,664 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/config"
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
mockRepo "backend/pkg/member/mock/repository"
|
||||
"backend/pkg/member/repository"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zeromicro/go-zero/core/stores/mon"
|
||||
"go.uber.org/mock/gomock"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestCreateUserAccount(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAccountRepository := mockRepo.NewMockAccountRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
Account: mockAccountRepository,
|
||||
Config: config.Config{
|
||||
Bcrypt: struct{ Cost int }{Cost: 10},
|
||||
},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.CreateLoginUserRequest
|
||||
mockSetup func()
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful account creation with Digimon platform",
|
||||
req: usecase.CreateLoginUserRequest{
|
||||
LoginID: "testuser",
|
||||
Token: "plaintext-password",
|
||||
Platform: member.Digimon,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Password encryption failure",
|
||||
req: usecase.CreateLoginUserRequest{
|
||||
LoginID: "testuser",
|
||||
Token: "plaintext-password",
|
||||
Platform: member.Digimon,
|
||||
},
|
||||
mockSetup: func() {
|
||||
HasPasswordFunc = func(password string, cost int) (string, error) {
|
||||
return "", errors.New("encryption error")
|
||||
}
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Duplicate account insertion error",
|
||||
req: usecase.CreateLoginUserRequest{
|
||||
LoginID: "testuser",
|
||||
Token: "plaintext-password",
|
||||
Platform: member.Digimon,
|
||||
},
|
||||
mockSetup: func() {
|
||||
HasPasswordFunc = func(password string, cost int) (string, error) {
|
||||
return "encrypted-password", nil
|
||||
}
|
||||
mockAccountRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(errors.New("duplicate account"))
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Successful account creation with non-Digimon platform",
|
||||
req: usecase.CreateLoginUserRequest{
|
||||
LoginID: "testuser",
|
||||
Platform: member.Google,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepository.EXPECT().Insert(gomock.Any(), gomock.Any()).Return(nil)
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
err := uc.CreateUserAccount(context.Background(), tt.req)
|
||||
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUIDByAccount(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAccountUIDRepo := mockRepo.NewMockAccountUIDRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
AccountUID: mockAccountUIDRepo,
|
||||
Config: config.Config{
|
||||
Bcrypt: struct{ Cost int }{Cost: 10},
|
||||
},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.GetUIDByAccountRequest
|
||||
mockSetup func()
|
||||
wantResp usecase.GetUIDByAccountResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successfully found UID by account",
|
||||
req: usecase.GetUIDByAccountRequest{
|
||||
Account: "testuser",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountUIDRepo.EXPECT().
|
||||
FindUIDByLoginID(gomock.Any(), "testuser").
|
||||
Return(&entity.AccountUID{UID: "12345"}, nil)
|
||||
},
|
||||
wantResp: usecase.GetUIDByAccountResponse{
|
||||
UID: "12345",
|
||||
Account: "testuser",
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Account not found",
|
||||
req: usecase.GetUIDByAccountRequest{
|
||||
Account: "notfounduser",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountUIDRepo.EXPECT().
|
||||
FindUIDByLoginID(gomock.Any(), "notfounduser").
|
||||
Return(nil, errs.NewError(
|
||||
code.CloudEPMember,
|
||||
code.CatResource,
|
||||
domain.FailedFindUIDByLoginIDErrorCode,
|
||||
"account not found",
|
||||
))
|
||||
},
|
||||
wantResp: usecase.GetUIDByAccountResponse{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error",
|
||||
req: usecase.GetUIDByAccountRequest{
|
||||
Account: "erroruser",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountUIDRepo.EXPECT().
|
||||
FindUIDByLoginID(gomock.Any(), "erroruser").
|
||||
Return(nil, errors.New("database error"))
|
||||
},
|
||||
wantResp: usecase.GetUIDByAccountResponse{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
resp, err := uc.GetUIDByAccount(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantResp, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserAccountInfo(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAccountRepo := mockRepo.NewMockAccountRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
Account: mockAccountRepo,
|
||||
Config: config.Config{
|
||||
Bcrypt: struct{ Cost int }{Cost: 10},
|
||||
},
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.GetUIDByAccountRequest
|
||||
mockSetup func()
|
||||
expected usecase.GetAccountInfoResponse
|
||||
wantErr bool
|
||||
errCode string
|
||||
}{
|
||||
{
|
||||
name: "Successfully found account",
|
||||
req: usecase.GetUIDByAccountRequest{Account: "testuser"},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().
|
||||
FindOneByAccount(gomock.Any(), "testuser").
|
||||
Return(&entity.Account{
|
||||
LoginID: "testuser",
|
||||
Platform: 1,
|
||||
Token: "testtoken",
|
||||
}, nil)
|
||||
},
|
||||
expected: usecase.GetAccountInfoResponse{
|
||||
Data: usecase.CreateLoginUserRequest{
|
||||
LoginID: "testuser",
|
||||
Platform: 1,
|
||||
Token: "testtoken",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Account not found",
|
||||
req: usecase.GetUIDByAccountRequest{Account: "notfounduser"},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().
|
||||
FindOneByAccount(gomock.Any(), "notfounduser").
|
||||
Return(nil, mon.ErrNotFound)
|
||||
},
|
||||
expected: usecase.GetAccountInfoResponse{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error",
|
||||
req: usecase.GetUIDByAccountRequest{Account: "erroruser"},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().
|
||||
FindOneByAccount(gomock.Any(), "erroruser").
|
||||
Return(nil, errors.New("database error"))
|
||||
},
|
||||
expected: usecase.GetAccountInfoResponse{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
resp, err := uc.GetUserAccountInfo(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserInfo(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepo,
|
||||
Config: config.Config{
|
||||
Bcrypt: struct{ Cost int }{Cost: 10},
|
||||
},
|
||||
})
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.GetUserInfoRequest
|
||||
mockSetup func()
|
||||
expected usecase.UserInfo
|
||||
wantErr bool
|
||||
errCode string
|
||||
}{
|
||||
{
|
||||
name: "Successfully found user by UID",
|
||||
req: usecase.GetUserInfoRequest{UID: "testUID"},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
FindOneByUID(gomock.Any(), "testUID").
|
||||
Return(&entity.User{
|
||||
UID: "testUID",
|
||||
Nickname: proto.String("testNick"),
|
||||
Email: proto.String("test@example.com"),
|
||||
PreferredLanguage: "en",
|
||||
}, nil)
|
||||
},
|
||||
expected: usecase.UserInfo{
|
||||
CreateUserInfoRequest: usecase.CreateUserInfoRequest{
|
||||
UID: "testUID",
|
||||
Nickname: proto.String("testNick"),
|
||||
Email: proto.String("test@example.com"),
|
||||
PreferredLanguage: "en",
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "User not found",
|
||||
req: usecase.GetUserInfoRequest{UID: "nonExistentUID"},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
FindOneByUID(gomock.Any(), "nonExistentUID").
|
||||
Return(nil, mon.ErrNotFound)
|
||||
},
|
||||
expected: usecase.UserInfo{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error while querying user by UID",
|
||||
req: usecase.GetUserInfoRequest{UID: "errorUID"},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
FindOneByUID(gomock.Any(), "errorUID").
|
||||
Return(nil, errors.New("database error"))
|
||||
},
|
||||
expected: usecase.UserInfo{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Successfully found user by Nickname",
|
||||
req: usecase.GetUserInfoRequest{NickName: "testNick"},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
FindOneByNickName(gomock.Any(), "testNick").
|
||||
Return(&entity.User{
|
||||
UID: "testUID",
|
||||
Nickname: proto.String("testNick"),
|
||||
Email: proto.String("test@example.com"),
|
||||
}, nil)
|
||||
},
|
||||
expected: usecase.UserInfo{
|
||||
CreateUserInfoRequest: usecase.CreateUserInfoRequest{
|
||||
UID: "testUID",
|
||||
Nickname: proto.String("testNick"),
|
||||
Email: proto.String("test@example.com"),
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid request, no UID or Nickname",
|
||||
req: usecase.GetUserInfoRequest{},
|
||||
mockSetup: func() {
|
||||
// No setup needed for invalid request
|
||||
},
|
||||
expected: usecase.UserInfo{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
resp, err := uc.GetUserInfo(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.expected, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserToken(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAccountRepo := mockRepo.NewMockAccountRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
Account: mockAccountRepo,
|
||||
})
|
||||
|
||||
HasPasswordFunc = func(password string, cost int) (string, error) {
|
||||
if password == "fail" {
|
||||
return "", errors.New("encryption error")
|
||||
}
|
||||
return "encrypted-password", nil
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.UpdateTokenRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
errCode string
|
||||
}{
|
||||
{
|
||||
name: "Successful token update",
|
||||
req: usecase.UpdateTokenRequest{Account: "testAccount", Token: "newPassword", Platform: 1},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().
|
||||
UpdateTokenByLoginID(gomock.Any(), "testAccount", "encrypted-password", gomock.Any()).
|
||||
Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Password encryption failure",
|
||||
req: usecase.UpdateTokenRequest{Account: "testAccount", Token: "fail", Platform: 1},
|
||||
mockSetup: func() {
|
||||
// No repo call expected
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Account not found",
|
||||
req: usecase.UpdateTokenRequest{Account: "nonExistentAccount", Token: "newPassword", Platform: 1},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().
|
||||
UpdateTokenByLoginID(gomock.Any(), "nonExistentAccount", "encrypted-password", gomock.Any()).
|
||||
Return(mon.ErrNotFound)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error during token update",
|
||||
req: usecase.UpdateTokenRequest{Account: "errorAccount", Token: "newPassword", Platform: 1},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().
|
||||
UpdateTokenByLoginID(gomock.Any(), "errorAccount", "encrypted-password", gomock.Any()).
|
||||
Return(errors.New("database error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
err := uc.UpdateUserToken(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberUseCase_UpdateUserInfo(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepo,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *usecase.UpdateUserInfoRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful update",
|
||||
req: &usecase.UpdateUserInfoRequest{
|
||||
UID: "testUID",
|
||||
Nickname: proto.String("UpdatedNick"),
|
||||
FullName: proto.String("Updated Name"),
|
||||
AvatarURL: proto.String("http://example.com/avatar.png"),
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
UpdateUserDetailsByUID(gomock.Any(), gomock.Any()).
|
||||
Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "User not found",
|
||||
req: &usecase.UpdateUserInfoRequest{
|
||||
UID: "nonExistentUID",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
UpdateUserDetailsByUID(gomock.Any(), gomock.Any()).
|
||||
Return(repository.ErrNotFound)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error",
|
||||
req: &usecase.UpdateUserInfoRequest{
|
||||
UID: "errorUID",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
UpdateUserDetailsByUID(gomock.Any(), gomock.Any()).
|
||||
Return(errors.New("database error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
err := uc.UpdateUserInfo(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberUseCase_UpdateStatus(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepo,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.UpdateStatusRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful status update",
|
||||
req: usecase.UpdateStatusRequest{UID: "testUID", Status: member.AccountStatusActive},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
UpdateStatus(gomock.Any(), "testUID", gomock.Any()).
|
||||
Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "User not found",
|
||||
req: usecase.UpdateStatusRequest{UID: "nonExistentUID", Status: member.AccountStatusActive},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
UpdateStatus(gomock.Any(), "nonExistentUID", gomock.Any()).
|
||||
Return(repository.ErrNotFound)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error",
|
||||
req: usecase.UpdateStatusRequest{UID: "errorUID", Status: member.AccountStatusUninitialized},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
UpdateStatus(gomock.Any(), "errorUID", gomock.Any()).
|
||||
Return(errors.New("database error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
err := uc.UpdateStatus(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMemberUseCase_ListMember(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockUserRepo := mockRepo.NewMockUserRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
User: mockUserRepo,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req usecase.ListUserInfoRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful member listing",
|
||||
req: usecase.ListUserInfoRequest{
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
ListMembers(gomock.Any(), gomock.Any()).
|
||||
Return([]*entity.User{
|
||||
{UID: "testUID1"},
|
||||
{UID: "testUID2"},
|
||||
}, int64(2), nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Database error",
|
||||
req: usecase.ListUserInfoRequest{
|
||||
PageSize: 10,
|
||||
PageIndex: 1,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockUserRepo.EXPECT().
|
||||
ListMembers(gomock.Any(), gomock.Any()).
|
||||
Return(nil, int64(0), errors.New("database error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
resp, err := uc.ListMember(context.Background(), tt.req)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, resp.Data)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// ErrInvalidPassword is returned when password validation fails
|
||||
var ErrInvalidPassword = errors.New("invalid password")
|
||||
|
||||
// HashPassword generates a bcrypt hash from the given password with the specified cost.
|
||||
// The cost parameter should be between 4 and 31, with higher values being more secure but slower.
|
||||
func HashPassword(password string, cost int) (string, error) {
|
||||
if password == "" {
|
||||
return "", ErrInvalidPassword
|
||||
}
|
||||
|
||||
if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
|
||||
cost = bcrypt.DefaultCost
|
||||
}
|
||||
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
// CheckPasswordHash compares a password with its bcrypt hash.
|
||||
// Returns true if the password matches the hash, false otherwise.
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
if password == "" || hash == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// GetHashingCost extracts the cost parameter from a bcrypt hash.
|
||||
// Returns the cost used to generate the hash, or 0 if the hash is invalid.
|
||||
func GetHashingCost(hashedPassword []byte) int {
|
||||
if len(hashedPassword) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
cost, err := bcrypt.Cost(hashedPassword)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return cost
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestHashPassword(t *testing.T) {
|
||||
password := "securepassword123"
|
||||
cost := bcrypt.DefaultCost
|
||||
|
||||
hashedPassword, err := HashPassword(password, cost)
|
||||
|
||||
assert.NoError(t, err, "生成哈希密碼應該成功")
|
||||
assert.NotEmpty(t, hashedPassword, "生成的哈希密碼不應為空")
|
||||
|
||||
// 確認哈希密碼長度符合預期
|
||||
assert.Greater(t, len(hashedPassword), 0, "哈希密碼長度應大於零")
|
||||
}
|
||||
|
||||
func TestCheckPasswordHash(t *testing.T) {
|
||||
password := "securepassword123"
|
||||
cost := bcrypt.DefaultCost
|
||||
|
||||
// 生成哈希密碼
|
||||
hashedPassword, err := HashPassword(password, cost)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 驗證密碼與哈希是否匹配
|
||||
assert.True(t, CheckPasswordHash(password, hashedPassword), "密碼應與哈希匹配")
|
||||
|
||||
// 測試不匹配的密碼
|
||||
wrongPassword := "wrongpassword"
|
||||
assert.False(t, CheckPasswordHash(wrongPassword, hashedPassword), "錯誤密碼不應與哈希匹配")
|
||||
}
|
||||
|
||||
func TestGetHashingCost(t *testing.T) {
|
||||
password := "securepassword123"
|
||||
cost := bcrypt.DefaultCost
|
||||
|
||||
// 生成哈希密碼
|
||||
hashedPassword, err := HashPassword(password, cost)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 驗證哈希成本
|
||||
actualCost := GetHashingCost([]byte(hashedPassword))
|
||||
assert.Equal(t, cost, actualCost, "哈希成本應與生成時的一致")
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
)
|
||||
|
||||
func (use *MemberUseCase) GenerateRefreshCode(ctx context.Context, param usecase.GenerateRefreshCodeRequest) (usecase.GenerateRefreshCodeResponse, error) {
|
||||
checkType, status := member.GetCodeNameByCode(param.CodeType)
|
||||
if !status {
|
||||
e := errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetVerifyCodeErrorCode,
|
||||
fmt.Sprintf("failed to get verify code type: %d", param.CodeType),
|
||||
)
|
||||
|
||||
return usecase.GenerateRefreshCodeResponse{}, e
|
||||
}
|
||||
|
||||
vc, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType)
|
||||
if err != nil {
|
||||
return usecase.GenerateRefreshCodeResponse{}, err
|
||||
}
|
||||
// 找不到,故要產生
|
||||
if vc == "" {
|
||||
vc, err = generateVerifyCode(6)
|
||||
if err != nil {
|
||||
return usecase.GenerateRefreshCodeResponse{}, errs.SystemInternalError(err.Error())
|
||||
}
|
||||
|
||||
err = use.VerifyCodeModel.SetVerifyCode(ctx, param.LoginID, checkType, vc)
|
||||
if err != nil {
|
||||
return usecase.GenerateRefreshCodeResponse{},
|
||||
errs.DatabaseErrorWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetCodeOnRedisErrorCode,
|
||||
"failed to set verify code",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return usecase.GenerateRefreshCodeResponse{
|
||||
Data: usecase.VerifyCode{
|
||||
VerifyCode: vc,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) VerifyRefreshCode(ctx context.Context, param usecase.VerifyRefreshCodeRequest) error {
|
||||
err := use.CheckRefreshCode(ctx, param)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 因為 CheckRefreshCde 中也有驗證一次,這次就讓他不報錯直接過,因為如果有錯誤,上面那個就會報錯了
|
||||
checkType, _ := member.GetCodeNameByCode(param.CodeType)
|
||||
// todo: 刪不掉看要用什麼方法補刪除,而不是報錯
|
||||
_ = use.VerifyCodeModel.DelVerifyCode(ctx, param.LoginID, checkType)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) CheckRefreshCode(ctx context.Context, param usecase.VerifyRefreshCodeRequest) error {
|
||||
checkType, status := member.GetCodeNameByCode(param.CodeType)
|
||||
if !status {
|
||||
return errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetVerifyCodeErrorCode,
|
||||
fmt.Sprintf("failed to get verify code type: %d", param.CodeType),
|
||||
)
|
||||
}
|
||||
|
||||
get, err := use.VerifyCodeModel.IsVerifyCodeExist(ctx, param.LoginID, checkType)
|
||||
if err != nil {
|
||||
return errs.DatabaseErrorWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetCodeOnRedisErrorCode,
|
||||
"failed to set verify code",
|
||||
)
|
||||
}
|
||||
if get == "" {
|
||||
return errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetCodeOnRedisErrorCode,
|
||||
"failed to get data",
|
||||
)
|
||||
}
|
||||
|
||||
if get != param.VerifyCode {
|
||||
return errs.ForbiddenWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToGetCodeCorrectErrorCode,
|
||||
"failed to verify code",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (use *MemberUseCase) VerifyPlatformAuthResult(ctx context.Context, param usecase.VerifyAuthResultRequest) (usecase.VerifyAuthResultResponse, error) {
|
||||
account, err := use.Account.FindOneByAccount(ctx, param.Account)
|
||||
if err != nil {
|
||||
return usecase.VerifyAuthResultResponse{}, errs.ResourceNotFoundWithScope(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToFindAccountErrorCode,
|
||||
fmt.Sprintf("failed to find account: %s", param.Account),
|
||||
)
|
||||
}
|
||||
|
||||
return usecase.VerifyAuthResultResponse{
|
||||
Status: CheckPasswordHash(param.Token, account.Token),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
)
|
||||
|
||||
func (use *MemberUseCase) VerifyGoogleAuthResult(ctx context.Context, req usecase.VerifyAuthResultRequest) (usecase.GoogleTokenInfo, error) {
|
||||
var tokenInfo usecase.GoogleTokenInfo
|
||||
// 發送 Google Token Info API 請求
|
||||
body, err := fetchGoogleTokenInfo(ctx, req.Token)
|
||||
if err != nil {
|
||||
return tokenInfo, err
|
||||
}
|
||||
|
||||
// 解析返回的 JSON 數據
|
||||
if err := json.Unmarshal(body, &tokenInfo); err != nil {
|
||||
return tokenInfo, errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogle,
|
||||
"failed to parse token info",
|
||||
)
|
||||
}
|
||||
|
||||
// 驗證 Token 資訊
|
||||
if err := validateGoogleTokenInfo(tokenInfo, use.Config.GoogleAuth.ClientID); err != nil {
|
||||
return tokenInfo, err
|
||||
}
|
||||
|
||||
return tokenInfo, nil
|
||||
}
|
||||
|
||||
// fetchGoogleTokenInfo 發送 Google TokenInfo API 請求並返回響應內容
|
||||
func fetchGoogleTokenInfo(ctx context.Context, token string) ([]byte, error) {
|
||||
uri := fmt.Sprintf("https://oauth2.googleapis.com/tokeninfo?id_token=%s", token)
|
||||
|
||||
// 發送請求
|
||||
r, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return nil, errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogle,
|
||||
"failed to create request", err.Error())
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleTimeout,
|
||||
"request timeout",
|
||||
)
|
||||
}
|
||||
|
||||
return nil, errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleTimeout,
|
||||
"failed to request Google TokenInfo API",
|
||||
)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 檢查返回的 HTTP 狀態碼
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleHTTPCode,
|
||||
fmt.Sprintf("unexpected status code: %d", resp.StatusCode),
|
||||
)
|
||||
}
|
||||
|
||||
// 讀取響應內容
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogle,
|
||||
"failed to read response body",
|
||||
)
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// validateGoogleTokenInfo 驗證 Google Token 資訊
|
||||
func validateGoogleTokenInfo(tokenInfo usecase.GoogleTokenInfo, expectedClientID string) error {
|
||||
// **驗證 1: Token 是否過期**
|
||||
expiration, err := strconv.ParseInt(tokenInfo.Exp, 10, 64)
|
||||
if err != nil || expiration <= time.Now().UTC().Unix() {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleTokenExpired,
|
||||
"token is expired",
|
||||
)
|
||||
}
|
||||
|
||||
// **驗證 2: Audience (aud) 是否與 Google Client ID 匹配**
|
||||
if tokenInfo.Aud != expectedClientID {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleInvalidAudience,
|
||||
"invalid audience",
|
||||
)
|
||||
}
|
||||
|
||||
// **驗證 3: 是否 email 已驗證**
|
||||
if tokenInfo.EmailVerified == "false" {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogle,
|
||||
"email is not verified",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateGoogleTokenInfo(t *testing.T) {
|
||||
expectedClientID := "test-client-id"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tokenInfo usecase.GoogleTokenInfo
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "Valid token",
|
||||
tokenInfo: usecase.GoogleTokenInfo{
|
||||
Exp: strconv.FormatInt(time.Now().UTC().Add(10*time.Minute).Unix(), 10),
|
||||
Aud: expectedClientID,
|
||||
EmailVerified: "true",
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "Token expired",
|
||||
tokenInfo: usecase.GoogleTokenInfo{
|
||||
Exp: strconv.FormatInt(time.Now().UTC().Add(-10*time.Minute).Unix(), 10),
|
||||
Aud: expectedClientID,
|
||||
EmailVerified: "true",
|
||||
},
|
||||
expectedErr: errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleTokenExpired,
|
||||
"token is expired",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Invalid audience",
|
||||
tokenInfo: usecase.GoogleTokenInfo{
|
||||
Exp: strconv.FormatInt(time.Now().UTC().Add(10*time.Minute).Unix(), 10),
|
||||
Aud: "invalid-client-id",
|
||||
EmailVerified: "true",
|
||||
},
|
||||
expectedErr: errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogleInvalidAudience,
|
||||
"invalid audience",
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Email not verified",
|
||||
tokenInfo: usecase.GoogleTokenInfo{
|
||||
Exp: strconv.FormatInt(time.Now().UTC().Add(10*time.Minute).Unix(), 10),
|
||||
Aud: expectedClientID,
|
||||
EmailVerified: "false",
|
||||
},
|
||||
expectedErr: errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyGoogle,
|
||||
"email is not verified",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := validateGoogleTokenInfo(test.tokenInfo, expectedClientID)
|
||||
|
||||
if test.expectedErr != nil {
|
||||
assert.NotNil(t, err)
|
||||
assert.EqualError(t, err, test.expectedErr.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"backend/pkg/member/domain"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"code.30cm.net/digimon/library-go/errs/code"
|
||||
)
|
||||
|
||||
// LineCodeToAccessToken 透過 Line 授權碼換取 Access Token
|
||||
func (use *MemberUseCase) LineCodeToAccessToken(ctx context.Context, code string) (usecase.LineAccessTokenResponse, error) {
|
||||
data := url.Values{
|
||||
"grant_type": {"authorization_code"},
|
||||
"code": {code},
|
||||
"redirect_uri": {use.Config.LineAuth.RedirectURI},
|
||||
"client_id": {use.Config.LineAuth.ClientID},
|
||||
"client_secret": {use.Config.LineAuth.ClientSecret},
|
||||
}
|
||||
|
||||
uri := "https://api.line.me/oauth2/v2.1/token"
|
||||
headers := map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
var tokenResponse usecase.LineAccessTokenResponse
|
||||
if err := use.doPost(ctx, uri, headers, data.Encode(), &tokenResponse); err != nil {
|
||||
return usecase.LineAccessTokenResponse{}, err
|
||||
}
|
||||
|
||||
return tokenResponse, nil
|
||||
}
|
||||
|
||||
// LineGetProfileByAccessToken 使用 Access Token 獲取 Line 用戶資料
|
||||
func (use *MemberUseCase) LineGetProfileByAccessToken(ctx context.Context, accessToken string) (*usecase.LineUserProfile, error) {
|
||||
uri := "https://api.line.me/v2/profile"
|
||||
headers := map[string]string{
|
||||
"Authorization": "Bearer " + accessToken,
|
||||
}
|
||||
|
||||
var profile usecase.LineUserProfile
|
||||
if err := use.doGet(ctx, uri, headers, &profile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
// doPost 發送 POST 請求並解析返回數據
|
||||
func (use *MemberUseCase) doPost(ctx context.Context, uri string, headers map[string]string, body string, result interface{}) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, bytes.NewBufferString(body))
|
||||
if err != nil {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyLine,
|
||||
"failed to create request",
|
||||
)
|
||||
}
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
return use.doRequest(req, result)
|
||||
}
|
||||
|
||||
// doGet 發送 GET 請求並解析返回數據
|
||||
func (use *MemberUseCase) doGet(ctx context.Context, uri string, headers map[string]string, result interface{}) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyLine,
|
||||
"failed to create request",
|
||||
)
|
||||
}
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
return use.doRequest(req, result)
|
||||
}
|
||||
|
||||
// doRequest 發送 HTTP 請求並處理響應
|
||||
func (use *MemberUseCase) doRequest(req *http.Request, result interface{}) error {
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyLine,
|
||||
"failed to send request",
|
||||
)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyLine,
|
||||
"unexpected status code: "+http.StatusText(resp.StatusCode),
|
||||
)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyLine,
|
||||
"failed to read response body",
|
||||
)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, result); err != nil {
|
||||
return errs.ThirdPartyError(
|
||||
code.CloudEPMember,
|
||||
domain.FailedToVerifyLine,
|
||||
"failed to parse response body",
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"backend/pkg/member/domain/entity"
|
||||
"backend/pkg/member/domain/member"
|
||||
"backend/pkg/member/domain/usecase"
|
||||
mockRepo "backend/pkg/member/mock/repository"
|
||||
|
||||
"code.30cm.net/digimon/library-go/errs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/mock/gomock"
|
||||
)
|
||||
|
||||
func TestGenerateRefreshCode(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockVerifyCodeModel := mockRepo.NewMockVerifyCodeRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
VerifyCodeModel: mockVerifyCodeModel,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
param usecase.GenerateRefreshCodeRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful code generation",
|
||||
param: usecase.GenerateRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", nil)
|
||||
mockVerifyCodeModel.EXPECT().SetVerifyCode(gomock.Any(), "testLoginID", "email", gomock.Any()).Return(nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Code type not found",
|
||||
param: usecase.GenerateRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: -999, // Invalid code type
|
||||
},
|
||||
mockSetup: func() {},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Existing code retrieval",
|
||||
param: usecase.GenerateRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("123456", nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Set verify code failure",
|
||||
param: usecase.GenerateRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", nil)
|
||||
mockVerifyCodeModel.EXPECT().SetVerifyCode(gomock.Any(), "testLoginID", "email", gomock.Any()).Return(errors.New("redis error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
resp, err := uc.GenerateRefreshCode(context.Background(), tt.param)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, resp)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, resp.Data.VerifyCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRefreshCode(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockVerifyCodeModel := mockRepo.NewMockVerifyCodeRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
VerifyCodeModel: mockVerifyCodeModel,
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
param usecase.VerifyRefreshCodeRequest
|
||||
mockSetup func()
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful verification",
|
||||
param: usecase.VerifyRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
VerifyCode: "123456",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("123456", nil)
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Code type not found",
|
||||
param: usecase.VerifyRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: -1, // Invalid CodeType
|
||||
VerifyCode: "123456",
|
||||
},
|
||||
mockSetup: func() {},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Code not found in Redis",
|
||||
param: usecase.VerifyRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
VerifyCode: "123456",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", nil)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Verification code mismatch",
|
||||
param: usecase.VerifyRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
VerifyCode: "654321", // Mismatch
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("123456", nil)
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Redis retrieval error",
|
||||
param: usecase.VerifyRefreshCodeRequest{
|
||||
LoginID: "testLoginID",
|
||||
CodeType: member.GenerateCodeTypeEmail,
|
||||
VerifyCode: "123456",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockVerifyCodeModel.EXPECT().IsVerifyCodeExist(gomock.Any(), "testLoginID", "email").Return("", errors.New("redis error"))
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
err := uc.CheckRefreshCode(context.Background(), tt.param)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyPlatformAuthResult(t *testing.T) {
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
|
||||
mockAccountRepo := mockRepo.NewMockAccountRepository(mockCtrl)
|
||||
uc := MustMemberUseCase(MemberUseCaseParam{
|
||||
Account: mockAccountRepo,
|
||||
})
|
||||
token, err := HashPassword("password", 10)
|
||||
assert.NoError(t, err)
|
||||
fmt.Println(token)
|
||||
tests := []struct {
|
||||
name string
|
||||
param usecase.VerifyAuthResultRequest
|
||||
mockSetup func()
|
||||
wantResp usecase.VerifyAuthResultResponse
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Successful verification",
|
||||
param: usecase.VerifyAuthResultRequest{
|
||||
Account: "testAccount",
|
||||
Token: "password",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "testAccount").Return(&entity.Account{
|
||||
Token: token,
|
||||
}, nil)
|
||||
},
|
||||
wantResp: usecase.VerifyAuthResultResponse{
|
||||
Status: true,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid token verification",
|
||||
param: usecase.VerifyAuthResultRequest{
|
||||
Account: "testAccount",
|
||||
Token: "invalidToken",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "testAccount").Return(&entity.Account{
|
||||
Token: "validToken",
|
||||
}, nil)
|
||||
},
|
||||
wantResp: usecase.VerifyAuthResultResponse{
|
||||
Status: false,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Account not found",
|
||||
param: usecase.VerifyAuthResultRequest{
|
||||
Account: "nonExistentAccount",
|
||||
Token: "someToken",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "nonExistentAccount").Return(nil, errs.ResourceNotFound("account not found"))
|
||||
},
|
||||
wantResp: usecase.VerifyAuthResultResponse{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Database error",
|
||||
param: usecase.VerifyAuthResultRequest{
|
||||
Account: "testAccount",
|
||||
Token: "someToken",
|
||||
},
|
||||
mockSetup: func() {
|
||||
mockAccountRepo.EXPECT().FindOneByAccount(gomock.Any(), "testAccount").Return(nil, errors.New("database error"))
|
||||
},
|
||||
wantResp: usecase.VerifyAuthResultResponse{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.mockSetup()
|
||||
|
||||
resp, err := uc.VerifyPlatformAuthResult(context.Background(), tt.param)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantResp, resp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue