From cccad97512ac97a702efd84d49ec12d849e08d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Wed, 26 Feb 2025 16:45:27 +0800 Subject: [PATCH] feat: add repository permission role role_permission user_role table --- Makefile | 7 +- go.mod | 33 ++ go.sum | 81 +++ pkg/domain/entity/policy.go | 28 - pkg/domain/entity/role.go | 23 +- pkg/domain/error.go | 12 +- pkg/domain/permission/status.go | 13 +- pkg/domain/permission/type.go | 2 + pkg/domain/rbac/rule.go | 2 +- pkg/domain/rbac/rule_test.go | 121 +++++ pkg/domain/repository/permission.go | 4 +- pkg/domain/repository/role.go | 52 +- pkg/domain/repository/role_permission.go | 19 + pkg/domain/repository/user_role.go | 26 + pkg/domain/token/additional_test.go | 54 ++ pkg/domain/token/scope_test.go | 27 + pkg/domain/token/type_test.go | 27 + pkg/domain/usecase/error.go | 7 + pkg/domain/usecase/role.go | 42 ++ pkg/domain/usecase/role_permission.go | 30 + pkg/domain/usecase/user_role.go | 21 + pkg/repository/permission.go | 4 +- pkg/repository/permission_test.go | 473 ++++++++++++++++ pkg/repository/role.go | 199 +++++++ pkg/repository/role_permission.go | 98 ++++ pkg/repository/role_permission_test.go | 265 +++++++++ pkg/repository/role_test.go | 541 +++++++++++++++++++ pkg/repository/start_mongo_container_test.go | 52 ++ pkg/repository/user_role.go | 137 +++++ pkg/repository/user_role_test.go | 336 ++++++++++++ pkg/usecase/casbin_redis_rbac.go | 166 +++--- pkg/usecase/casbin_redis_rbac_test.go | 1 + pkg/usecase/permission_tree.go | 229 ++++++++ pkg/usecase/permission_tree_test.go | 358 ++++++++++++ 34 files changed, 3341 insertions(+), 149 deletions(-) delete mode 100644 pkg/domain/entity/policy.go create mode 100644 pkg/domain/rbac/rule_test.go create mode 100644 pkg/domain/repository/role_permission.go create mode 100644 pkg/domain/repository/user_role.go create mode 100644 pkg/domain/token/additional_test.go create mode 100644 pkg/domain/token/scope_test.go create mode 100644 pkg/domain/token/type_test.go create mode 100644 pkg/domain/usecase/error.go create mode 100644 pkg/domain/usecase/role.go create mode 100644 pkg/domain/usecase/role_permission.go create mode 100644 pkg/domain/usecase/user_role.go create mode 100644 pkg/repository/permission_test.go create mode 100644 pkg/repository/role.go create mode 100644 pkg/repository/role_permission.go create mode 100644 pkg/repository/role_permission_test.go create mode 100644 pkg/repository/role_test.go create mode 100644 pkg/repository/start_mongo_container_test.go create mode 100644 pkg/repository/user_role.go create mode 100644 pkg/repository/user_role_test.go create mode 100644 pkg/usecase/casbin_redis_rbac_test.go create mode 100644 pkg/usecase/permission_tree.go create mode 100644 pkg/usecase/permission_tree_test.go diff --git a/Makefile b/Makefile index 30f9486..cad4eee 100644 --- a/Makefile +++ b/Makefile @@ -49,12 +49,13 @@ build-docker: .PHONY: mock-gen mock-gen: # 建立 mock 資料 mockgen -source=./pkg/domain/repository/token.go -destination=./pkg/mock/repository/token.go -package=mock + mockgen -source=./pkg/domain/repository/permission.go -destination=./pkg/mock/repository/permission.go -package=mock + mockgen -source=./pkg/domain/repository/role.go -destination=./pkg/mock/repository/role.go -package=mock + mockgen -source=./pkg/domain/repository/role_permission.go -destination=./pkg/mock/repository/role_permission.go -package=mock + mockgen -source=./pkg/domain/repository/user_role.go -destination=./pkg/mock/repository/user_role.go -package=mock @echo "Generate mock files successfully" - - @echo "Generate mock files successfully" - .PHONY: migrate-database migrate-database: migrate -source file://generate/database/mysql -database 'mysql://root:yytt@tcp(127.0.0.1:3306)/digimon_permission' up diff --git a/go.mod b/go.mod index 8d75033..08255d4 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.1 github.com/segmentio/ksuid v1.0.4 github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.34.0 github.com/zeromicro/go-zero v1.8.0 go.mongodb.org/mongo-driver v1.17.2 go.uber.org/mock v0.5.0 @@ -18,20 +19,33 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/casbin/govaluate v1.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/containerd v1.7.18 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // 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 v27.1.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect @@ -47,32 +61,51 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // 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/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.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.0 // 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.20.5 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/redis/go-redis/v9 v9.7.0 // indirect + github.com/shirou/gopsutil/v3 v3.23.12 // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shopspring/decimal v1.4.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spaolacci/murmur3 v1.1.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.3 // indirect go.etcd.io/etcd/api/v3 v3.5.15 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.15 // indirect go.etcd.io/etcd/client/v3 v3.5.15 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.34.0 // indirect go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect diff --git a/go.sum b/go.sum index e61a8e8..50ee797 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,14 @@ code.30cm.net/digimon/library-go/errs v1.2.14 h1:Un9wcIIjjJW8D2i0ISf8ibzp9oNT4Oq 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= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE= github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0= @@ -24,26 +32,48 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/containerd v1.7.18 h1:jqjZTQNfXGoEaZdW1WwPU0RqSn1Bm2Ay/KJPUuO8nao= +github.com/containerd/containerd v1.7.18/go.mod h1:IYEk9/IO6wAPUz2bCMVUbsfXjzw5UNP5fLz4PsUygQ4= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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 v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= +github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 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.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= @@ -67,6 +97,7 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -98,6 +129,10 @@ 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.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -105,6 +140,16 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk 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/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -112,12 +157,18 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +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.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 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= @@ -126,6 +177,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/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.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= @@ -142,8 +195,16 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4= +github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 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/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -154,6 +215,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -161,6 +223,12 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/testcontainers/testcontainers-go v0.34.0 h1:5fbgF0vIN5u+nD3IWabQwRybuB4GY8G2HHgCkbMzMHo= +github.com/testcontainers/testcontainers-go v0.34.0/go.mod h1:6P/kMkQe8yqPHfPWNulFGdFHTD8HB2vLq/231xY2iPQ= +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= @@ -175,6 +243,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 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.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= +github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zeromicro/go-zero v1.8.0 h1:4g/8VW+fOyM51HZYPeI3mXIZdEX+Fl6SsdYX2H5PYw4= github.com/zeromicro/go-zero v1.8.0/go.mod h1:xDBF+/iDzj30zPvu6HNUIbpz1J6+/g3Sx9D/DytJfss= go.etcd.io/etcd/api/v3 v3.5.15 h1:3KpLJir1ZEBrYuV2v+Twaa/e2MdDCEZ/70H+lzEiwsk= @@ -187,6 +257,8 @@ go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793Sqyh go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= @@ -254,15 +326,22 @@ golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -311,6 +390,8 @@ 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.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= k8s.io/apimachinery v0.29.4 h1:RaFdJiDmuKs/8cm1M6Dh1Kvyh59YQFDcFuFTSmXes6Q= diff --git a/pkg/domain/entity/policy.go b/pkg/domain/entity/policy.go deleted file mode 100644 index 1f8ca6f..0000000 --- a/pkg/domain/entity/policy.go +++ /dev/null @@ -1,28 +0,0 @@ -package entity - -import ( - "go.mongodb.org/mongo-driver/bson/primitive" -) - -//TODO 未來才實作(要做IAM 時) - -// Policy 表示一個存取控制策略文件 -type Policy struct { - ID primitive.ObjectID `bson:"_id,omitempty"` // MongoDB 的主鍵 - Name string `bson:"name"` // 策略名稱 - Description string `bson:"description,omitempty"` // 策略描述 - Effect string `bson:"effect"` // "allow" 或 "deny" - Condition interface{} `bson:"condition,omitempty"` // 存放 JSON 條件,可以是 map[string]interface{} 或其他符合 BSON 格式的資料 - CreatedAt int64 `bson:"created_at"` // 建立時間 - UpdatedAt int64 `bson:"updated_at"` // 更新時間 -} - -// Resource 表示需要保護的資源 -type Resource struct { - ID primitive.ObjectID `bson:"_id,omitempty"` // MongoDB 的主鍵 - Name string `bson:"name"` // 資源名稱 - Type string `bson:"type"` // 資源類型 - Description string `bson:"description,omitempty"` // 資源描述 - CreatedAt int64 `bson:"created_at"` // 建立時間 - UpdatedAt int64 `bson:"updated_at"` // 更新時間 -} diff --git a/pkg/domain/entity/role.go b/pkg/domain/entity/role.go index 2585395..9e76b8d 100644 --- a/pkg/domain/entity/role.go +++ b/pkg/domain/entity/role.go @@ -5,9 +5,30 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) +// +//1. UID (使用者ID) +// +//這個欄位通常代表創建這個角色的使用者,可能的用途: +//• 追蹤角色的建立者:確保知道誰創建了這個角色。 +//• 限定角色的管理範圍:例如,只有 UID 對應的使用者可以修改或刪除這個角色。 +//• 支援多租戶 (Multi-Tenancy):如果一個 UID 只能看到自己創建的角色,那這個系統可能是多租戶架構的一部分。 +// +//2. ClientID (客戶端ID) +// +//這個欄位通常代表該角色所屬的應用程式或客戶端,可能的用途: +//• 多租戶架構 (Multi-Tenancy):不同 ClientID 的角色可能互相隔離,確保不同組織不會影響彼此的角色權限。 +//• 對應 OAuth 2.0 或 OpenID Connect: +//• 在 OAuth2 / OIDC 的架構下,每個應用程式 (client_id) 可能有不同的角色和權限。 +//• 例如,client_id 為 web-app-1 的角色與 client_id 為 mobile-app-1 的角色可能完全不同。 +//• API 權限控制: +//• ClientID 可能是用來限制某些角色只能在特定應用程式中被使用,例如 Web 端和行動端的角色不同。 + +// Role 是這樣,如果有綁定某個 UID type Role struct { ID primitive.ObjectID `bson:"_id,omitempty"` - Name string `bson:"name"` + Name string `bson:"name"` // 角色名稱 + UID string `bson:"uid"` + ClientID string `bson:"client_id"` Status permission.Status `bson:"status"` // 例如 1: 啟用, 0: 停用 CreateAt int64 `bson:"create_at"` UpdateAt int64 `bson:"update_at"` diff --git a/pkg/domain/error.go b/pkg/domain/error.go index b89ca28..140f571 100644 --- a/pkg/domain/error.go +++ b/pkg/domain/error.go @@ -11,7 +11,6 @@ import ( const ( TokenServerErrorCode = 1 + iota - TokenServerRedisErrorCode TokenValidateErrorCode TokenClaimErrorCode TokenCreateErrorCode @@ -19,17 +18,8 @@ const ( TokenCancelErrorCode TokensCancelErrorCode TokenGetErrorCode - NewOneTokenErrorCode - DelOneTokenErrorCode - SendTooShortErrorCode - SetForgetPasswordRedisErrorCode - FailedToGetCorrectVerifyCode - SendVerifyCodeRedisErrorCode - GenerateVerifyCodeRedisErrorCode - FailedToCheckVerifyCode - AccountPlatformNotCorrectErrorCode - PermissionDeleteErrorCode + FailedToGetRolePermission ) func TokenError(ec ers.ErrorCode, s ...string) *ers.LibError { diff --git a/pkg/domain/permission/status.go b/pkg/domain/permission/status.go index 728af61..b9a6023 100644 --- a/pkg/domain/permission/status.go +++ b/pkg/domain/permission/status.go @@ -8,20 +8,23 @@ const ( ) const ( - ClosePermission string = "close" - OpenPermission string = "open" + ClosePermission StatusCode = "close" + OpenPermission StatusCode = "open" ) func (s Status) String() string { status, ok := statusMap[s] if ok { - return status + return string(status) } - return ClosePermission + return string(ClosePermission) } -var statusMap = map[Status]string{ +var statusMap = map[Status]StatusCode{ Open: OpenPermission, Close: ClosePermission, } + +type StatusCode string +type Permissions map[string]StatusCode diff --git a/pkg/domain/permission/type.go b/pkg/domain/permission/type.go index a50e05a..02b0fa6 100644 --- a/pkg/domain/permission/type.go +++ b/pkg/domain/permission/type.go @@ -6,3 +6,5 @@ const ( BackendUser Type = iota + 1 FrontendUser ) + +const AdminRoleUID = "GodDog" diff --git a/pkg/domain/rbac/rule.go b/pkg/domain/rbac/rule.go index 99a95c4..299f1a0 100644 --- a/pkg/domain/rbac/rule.go +++ b/pkg/domain/rbac/rule.go @@ -24,7 +24,7 @@ func (rule Rule) ToString() []string { rule.Field2, rule.Field3, rule.Field4, rule.Field5, } // 移除空字串,提高效能 - var result []string + result := make([]string, 0, len(fields)) for _, field := range fields { if field != "" { result = append(result, field) diff --git a/pkg/domain/rbac/rule_test.go b/pkg/domain/rbac/rule_test.go new file mode 100644 index 0000000..73c74a1 --- /dev/null +++ b/pkg/domain/rbac/rule_test.go @@ -0,0 +1,121 @@ +package rbac + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRule_ToString(t *testing.T) { + tests := []struct { + name string + input Rule + expected []string + }{ + { + name: "完整六個欄位", + input: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "data1", + Field2: "read", + Field3: "extra1", + Field4: "extra2", + Field5: "extra3", + }, + expected: []string{"p", "admin", "data1", "read", "extra1", "extra2", "extra3"}, + }, + { + name: "部分欄位空白", + input: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "", + Field2: "read", + Field3: "", + Field4: "", + Field5: "extra3", + }, + expected: []string{"p", "admin", "read", "extra3"}, + }, + { + name: "只有 PolicyType", + input: Rule{PolicyType: "p"}, + expected: []string{"p"}, + }, + { + name: "所有欄位皆空", + input: Rule{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.input.ToString() + assert.Equal(t, tt.expected, result, "ToString output should match expected") + }) + } +} + +func TestStringToPolicy(t *testing.T) { + tests := []struct { + name string + policyType string + input []string + expected Rule + }{ + { + name: "完整六個欄位", + policyType: "p", + input: []string{"admin", "data1", "read", "extra1", "extra2", "extra3"}, + expected: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "data1", + Field2: "read", + Field3: "extra1", + Field4: "extra2", + Field5: "extra3", + }, + }, + { + name: "部分欄位空白", + policyType: "p", + input: []string{"admin", "", "read", "", "", "extra3"}, + expected: Rule{ + PolicyType: "p", + Field0: "admin", + Field1: "", + Field2: "read", + Field3: "", + Field4: "", + Field5: "extra3", + }, + }, + { + name: "只有 PolicyType", + policyType: "p", + input: []string{}, + expected: Rule{ + PolicyType: "p", + }, + }, + { + name: "只有兩個欄位", + policyType: "g", + input: []string{"user", "admin"}, + expected: Rule{ + PolicyType: "g", + Field0: "user", + Field1: "admin", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := StringToPolicy(tt.policyType, tt.input) + assert.Equal(t, tt.expected, result, "StringToPolicy output should match expected") + }) + } +} diff --git a/pkg/domain/repository/permission.go b/pkg/domain/repository/permission.go index 90faa74..ed8c122 100644 --- a/pkg/domain/repository/permission.go +++ b/pkg/domain/repository/permission.go @@ -18,8 +18,8 @@ type PermissionRepository interface { type GetPermission interface { // GetAll 取得所有權限列表 GetAll(ctx context.Context, status *permission.Status) ([]entity.Permission, error) - // GetAllByID 以權限 ID 作為鍵取得所有權限的映射 - GetAllByID(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) + // GetAllIntoIDMap 以權限 ID 作為鍵取得所有權限的映射 + GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) // FindOne 根據查詢條件取得單筆權限資料 FindOne(ctx context.Context, query PermissionQuery) (entity.Permission, error) // FindByNames 根據權限名稱列表查詢權限資料 diff --git a/pkg/domain/repository/role.go b/pkg/domain/repository/role.go index 21535e7..bde3a61 100644 --- a/pkg/domain/repository/role.go +++ b/pkg/domain/repository/role.go @@ -2,41 +2,39 @@ package repository import ( "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" "context" + "go.mongodb.org/mongo-driver/mongo" ) type RoleRepository interface { - Insert(ctx context.Context, role *entity.Role) error - Update(ctx context.Context, role *entity.Role) error - Delete(ctx context.Context, role *entity.Role) error - List() - ListAll() - Get() - GetByUID() + List(ctx context.Context, param ListQuery) ([]*entity.Role, int64, error) + GetByID(ctx context.Context, id string) (*entity.Role, error) + GetByUID(ctx context.Context, uid string) (*entity.Role, error) + All(ctx context.Context, clientID *string) ([]*entity.Role, error) + Create(ctx context.Context, role *entity.Role) error + Update(ctx context.Context, data UpdateReq) error + Delete(ctx context.Context, id string) error + RoleIndex } -type RoleRepository interface { - Page(ctx context.Context, filter PageRoleFilter, page, size int) ([]entity.Role, int64, error) - Get(ctx context.Context, id int64) (entity.Role, error) - GetByUID(ctx context.Context, uid string) (entity.Role, error) - All(ctx context.Context, clientID int) ([]entity.Role, error) - IncrementID(ctx context.Context) (int, error) - Create(ctx context.Context, role *entity.Role) (int64, error) - Update(ctx context.Context, role *entity.Role) error - Delete(ctx context.Context, uid string) error +type RoleIndex interface { + Index20250224UP(ctx context.Context) (*mongo.Cursor, error) } -type PageRoleFilter struct { - ClientID int - UID string - Name string - Permissions []string - Status int +type ListQuery struct { + PageSize int64 // 必填 + PageIndex int64 // 必填 + ClientID *string + UID *string + Name *string + Status *permission.Status } -type RolePermissionRepository interface { - BindRolePermission() - UnBindRolePermission() - GetRolePermission() - GetByPermissionID() +type UpdateReq struct { + ID string + Name *string + UID *string + ClientID *string + Status *permission.Status } diff --git a/pkg/domain/repository/role_permission.go b/pkg/domain/repository/role_permission.go new file mode 100644 index 0000000..c0611a6 --- /dev/null +++ b/pkg/domain/repository/role_permission.go @@ -0,0 +1,19 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "context" + "go.mongodb.org/mongo-driver/mongo" +) + +type RolePermissionRepository interface { + Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error) + GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error) + Create(ctx context.Context, entity entity.RolePermission) error + Delete(ctx context.Context, roleID string, permission string) error + RolePermissionIndex +} + +type RolePermissionIndex interface { + Index20250225UP(ctx context.Context) (*mongo.Cursor, error) +} diff --git a/pkg/domain/repository/user_role.go b/pkg/domain/repository/user_role.go new file mode 100644 index 0000000..b589355 --- /dev/null +++ b/pkg/domain/repository/user_role.go @@ -0,0 +1,26 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "context" + "go.mongodb.org/mongo-driver/mongo" +) + +type UserRoleRepository interface { + GetAll(ctx context.Context) ([]*entity.UserRole, error) // `All` -> `GetAll`,更直觀 + GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) // `Get` -> `GetByUserID`,明確是透過 `uid` 查詢 + GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) // `GetByRoleID` -> `GetUsersByRoleID`,更具語意 + CountUsersByRole(ctx context.Context) ([]RoleUserCount, error) // `UserCount` -> `CountUsersByRole`,清楚表達它是計算使用者數量 + CreateUserRole(ctx context.Context, param entity.UserRole) error // `Create` -> `CreateUserRole`,明確表示新增的是使用者與角色的關係 + UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) // `Update` -> `UpdateUserRole`,明確描述更新的是哪個對象 + UserRoleIndex +} + +type UserRoleIndex interface { + Index20250225UP(ctx context.Context) (*mongo.Cursor, error) +} + +type RoleUserCount struct { + RoleID string `bson:"_id"` + Count int `bson:"count"` +} diff --git a/pkg/domain/token/additional_test.go b/pkg/domain/token/additional_test.go new file mode 100644 index 0000000..daf7683 --- /dev/null +++ b/pkg/domain/token/additional_test.go @@ -0,0 +1,54 @@ +package token + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAdditional_String(t *testing.T) { + tests := []struct { + name string + input Additional + expected string + }{ + {"ID to String", ID, "id"}, + {"Role to String", Role, "role"}, + {"Device to String", Device, "device"}, + {"UID to String", UID, "uid"}, + {"Account to String", Account, "account"}, + {"Scope to String", Scope, "scope"}, + {"Type to String", Type, "token_type"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.input.String() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsValidAdditional(t *testing.T) { + validKeys := []Additional{ + ID, Role, Device, + UID, Account, Scope, Type, + } + + invalidKeys := []Additional{ + "invalid", "unknown", "random", "test", + } + + // 測試有效 Key + for _, key := range validKeys { + t.Run("ValidKey_"+key.String(), func(t *testing.T) { + assert.True(t, IsValidAdditional(key)) + }) + } + + // 測試無效 Key + for _, key := range invalidKeys { + t.Run("InvalidKey_"+string(key), func(t *testing.T) { + assert.False(t, IsValidAdditional(key)) + }) + } +} diff --git a/pkg/domain/token/scope_test.go b/pkg/domain/token/scope_test.go new file mode 100644 index 0000000..fe2df14 --- /dev/null +++ b/pkg/domain/token/scope_test.go @@ -0,0 +1,27 @@ +package token + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestTScope_ToString(t *testing.T) { + tests := []struct { + name string + input TScope + expected string + }{ + {"Empty String", TScope(""), ""}, + {"Simple String", TScope("read"), "read"}, + {"Complex String", TScope("user:write"), "user:write"}, + {"Special Characters", TScope("@dmin!"), "@dmin!"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 使用指標調用方法 + result := tt.input.ToString() + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/domain/token/type_test.go b/pkg/domain/token/type_test.go new file mode 100644 index 0000000..6eb449e --- /dev/null +++ b/pkg/domain/token/type_test.go @@ -0,0 +1,27 @@ +package token + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestType_ToString(t *testing.T) { + tests := []struct { + name string + input VerifyType + expected string + }{ + {"Empty String", VerifyType(""), ""}, + {"Simple String", VerifyType("read"), "read"}, + {"Complex String", VerifyType("user:write"), "user:write"}, + {"Special Characters", VerifyType("@dmin!"), "@dmin!"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 使用指標調用方法 + result := tt.input.ToString() + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/domain/usecase/error.go b/pkg/domain/usecase/error.go new file mode 100644 index 0000000..ae72163 --- /dev/null +++ b/pkg/domain/usecase/error.go @@ -0,0 +1,7 @@ +package usecase + +import "fmt" + +var ( + NotFoundError = fmt.Errorf("permission not found") +) diff --git a/pkg/domain/usecase/role.go b/pkg/domain/usecase/role.go new file mode 100644 index 0000000..4680135 --- /dev/null +++ b/pkg/domain/usecase/role.go @@ -0,0 +1,42 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "context" +) + +type RoleUseCase interface { + List(ctx context.Context, param ListQuery) ([]Role, int64, error) + All(ctx context.Context, clientID *string) ([]Role, error) + GetByID(ctx context.Context, id string) (*Role, error) + GetByUID(ctx context.Context, uid string) (*Role, error) + Create(ctx context.Context, role CreateRoleReq) error + Update(ctx context.Context, id string, data CreateRoleReq) error + Delete(ctx context.Context, id string) error +} + +type ListQuery struct { + PageSize int64 // 必填 + PageIndex int64 // 必填 + ClientID *string + UID *string + Name *string + Status *permission.Status +} + +type Role struct { + ID string `json:"id"` + Name string `json:"name"` // 角色名稱 + UID string `json:"uid"` + ClientID string `json:"client_id"` + Status permission.Status `json:"status"` // 例如 1: 啟用, 0: 停用 + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` +} + +type CreateRoleReq struct { + Name *string `json:"name"` // 角色名稱 + UID *string `json:"uid"` + ClientID *string `json:"client_id"` + Status *permission.Status `json:"status"` // 例如 1: 啟用, 0: 停用 +} diff --git a/pkg/domain/usecase/role_permission.go b/pkg/domain/usecase/role_permission.go new file mode 100644 index 0000000..7491343 --- /dev/null +++ b/pkg/domain/usecase/role_permission.go @@ -0,0 +1,30 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "context" +) + +type RolePermissionUseCase interface { + Get(ctx context.Context, roleID int64) (permission.Permissions, error) + GetByRoleUID(ctx context.Context, uid string) (permission.Permissions, error) + GetByUser(ctx context.Context, uid string) (UserPermission, error) + Create(ctx context.Context, roleID int64, permissions permission.Permissions) error + Delete(ctx context.Context, roleID int64, permissions permission.Permissions) error + List(ctx context.Context, req ListQuery) (RoleResp, error) +} + +type UserPermission struct { + RoleID string `json:"role_id"` + Permissions permission.Permissions `json:"permissions"` +} + +type UserRoleCountResp struct { + Role + UserCount int `json:"user_count"` +} + +type RoleResp struct { + List []UserRoleCountResp `json:"list"` + Total int64 `json:"total"` +} diff --git a/pkg/domain/usecase/user_role.go b/pkg/domain/usecase/user_role.go new file mode 100644 index 0000000..e8194e1 --- /dev/null +++ b/pkg/domain/usecase/user_role.go @@ -0,0 +1,21 @@ +package usecase + +import "context" + +type UserRoleUseCase interface { + Select(ctx context.Context, filter UserRoleFilter) ([]UserRole, error) + Get(ctx context.Context, uid string) (UserRole, error) + Create(ctx context.Context, uid, roleID string) (UserRole, error) + Delete(ctx context.Context, uid, roleID string) error +} + +type UserRole struct { + UID string `json:"uid"` + RoleID string `json:"role_id"` + CreateAt int64 `json:"create_at"` + UpdateAt int64 `json:"update_at"` +} + +type UserRoleFilter struct { + RoleID string +} diff --git a/pkg/repository/permission.go b/pkg/repository/permission.go index dcdd636..e207be3 100644 --- a/pkg/repository/permission.go +++ b/pkg/repository/permission.go @@ -123,7 +123,7 @@ func (repo *PermissionRepository) GetAll(ctx context.Context, status *permission return result, nil } -func (repo *PermissionRepository) GetAllByID(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) { +func (repo *PermissionRepository) GetAllIntoIDMap(ctx context.Context, status *permission.Status) (map[string]entity.Permission, error) { permissions, err := repo.GetAll(ctx, status) if err != nil { return nil, err @@ -153,7 +153,7 @@ func (repo *PermissionRepository) FindByNames(ctx context.Context, names []strin result := make([]entity.Permission, 0) // 使用 $in 操作符查詢 name 在 names 切片中的文件 filter := bson.M{"name": bson.M{"$in": names}} - err := repo.DB.GetClient().FindOne(ctx, &result, filter) + err := repo.DB.GetClient().Find(ctx, &result, filter) if err != nil { return []entity.Permission{}, err } diff --git a/pkg/repository/permission_test.go b/pkg/repository/permission_test.go new file mode 100644 index 0000000..7d68863 --- /dev/null +++ b/pkg/repository/permission_test.go @@ -0,0 +1,473 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "testing" +) + +func SetupTestPermissionRepository(db string) (repository.PermissionRepository, 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 := PermissionRepositoryParam{ + Conf: conf, + } + repo := NewPermissionRepository(param) + _, _ = repo.Index20250214UP(context.Background()) + + return repo, tearDown, nil +} + +// 測試 Insert +func TestPermissionRepository_Insert(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input entity.Permission + expectErr bool + }{ + { + name: "成功插入", + input: entity.Permission{ + Name: "test-permission", + HTTPMethod: "GET", + HTTPPath: "/test", + Status: 1, + Type: permission.BackendUser, + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Insert(context.Background(), tc.input) + if tc.expectErr { + assert.NotNil(t, err, "應該要返回錯誤") + } else { + assert.Nil(t, err, "不應該返回錯誤") + } + }) + } +} + +func TestPermissionRepository_Update(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先插入一條測試數據 + existingPermission := entity.Permission{ + Name: "original-permission", + HTTPMethod: "GET", + HTTPPath: "/original", + Status: 1, + Type: permission.BackendUser, + } + err = repo.Insert(context.Background(), existingPermission) + assert.Nil(t, err, "插入初始數據失敗") + + // 取得剛插入的 ID + found, err := repo.FindOne(context.Background(), repository.PermissionQuery{ + HTTPMethod: existingPermission.HTTPMethod, + HTTPPath: existingPermission.HTTPPath, + }) + assert.Nil(t, err, "應該能找到插入的權限") + id := found.ID.Hex() + + testCases := []struct { + name string + id string + updateReq repository.UpdatePermission + expectErr bool + }{ + { + name: "成功更新名稱", + id: id, + updateReq: repository.UpdatePermission{ + Name: ToPointer("updated-name"), + }, + expectErr: false, + }, + { + name: "成功更新 HTTP 方法", + id: id, + updateReq: repository.UpdatePermission{ + HTTPMethod: ToPointer("PUT"), + }, + expectErr: false, + }, + { + name: "成功更新 HTTP 路徑", + id: id, + updateReq: repository.UpdatePermission{ + HTTPPath: ToPointer("/updated-path"), + }, + expectErr: false, + }, + { + name: "成功更新狀態", + id: id, + updateReq: repository.UpdatePermission{ + Status: ToPointer(permission.Open), + }, + expectErr: false, + }, + { + name: "成功更新多個欄位", + id: id, + updateReq: repository.UpdatePermission{ + Name: ToPointer("multi-updated"), + HTTPMethod: ToPointer("PATCH"), + HTTPPath: ToPointer("/multi-update"), + Status: ToPointer(permission.Close), + }, + expectErr: false, + }, + { + name: "更新失敗 - 錯誤的 ObjectID", + id: "invalid", + updateReq: repository.UpdatePermission{ + Name: ToPointer("invalid-update"), + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Update(context.Background(), tc.id, tc.updateReq) + if tc.expectErr { + assert.NotNil(t, err, "應該要返回錯誤") + } else { + assert.Nil(t, err, "不應該返回錯誤") + + // 確保更新後的資料正確 + updated, err := repo.GetAll(context.Background(), nil) + assert.Nil(t, err, "應該能找到更新後的權限") + + if tc.updateReq.Name != nil { + assert.Equal(t, *tc.updateReq.Name, updated[0].Name, "名稱應該被更新") + } + if tc.updateReq.HTTPMethod != nil { + assert.Equal(t, *tc.updateReq.HTTPMethod, updated[0].HTTPMethod, "HTTP 方法應該被更新") + } + if tc.updateReq.HTTPPath != nil { + assert.Equal(t, *tc.updateReq.HTTPPath, updated[0].HTTPPath, "HTTP 路徑應該被更新") + } + if tc.updateReq.Status != nil { + assert.Equal(t, *tc.updateReq.Status, updated[0].Status, "狀態應該被更新") + } + } + }) + } +} + +// 測試 Delete 方法 +func TestPermissionRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + permission := entity.Permission{ + ID: primitive.NewObjectID(), + Name: "delete-test", + HTTPMethod: "DELETE", + HTTPPath: "/test-delete", + Status: 1, + } + + err = repo.Insert(context.Background(), permission) + assert.NoError(t, err, "插入測試數據時不應發生錯誤") + + testCases := []struct { + name string + id string + expectErr bool + }{ + { + name: "成功刪除存在的權限", + id: permission.ID.Hex(), + expectErr: false, + }, + { + name: "刪除不存在的權限", + id: primitive.NewObjectID().Hex(), + expectErr: false, // MongoDB 刪除不存在的 ID 仍然不會報錯 + }, + { + name: "刪除無效的 ObjectID", + id: "invalid-object-id", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Delete(context.Background(), tc.id) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + }) + } +} + +// 測試 GetAll 方法 +func TestPermissionRepository_GetAll(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testPermissions := []entity.Permission{ + { + ID: primitive.NewObjectID(), + Name: "read", + HTTPMethod: "GET", + HTTPPath: "/read", + Status: permission.Open, + }, + { + ID: primitive.NewObjectID(), + Name: "write", + HTTPMethod: "POST", + HTTPPath: "/write", + Status: permission.Close, + }, + } + + for _, p := range testPermissions { + err := repo.Insert(context.Background(), p) + assert.NoError(t, err, "插入測試數據時不應該發生錯誤") + } + + testCases := []struct { + name string + status *permission.Status + expectLen int + expectErr bool + }{ + { + name: "查詢所有權限", + status: nil, + expectLen: 2, + expectErr: false, + }, + { + name: "查詢開啟的權限", + status: ToPointer(permission.Open), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢關閉的權限", + status: ToPointer(permission.Close), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢不存在的權限狀態", + status: ToPointer(permission.Status(-1)), + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetAll(context.Background(), tc.status) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "查詢結果數量不符合預期") + } + }) + } +} + +// 測試 GetAllIntoIDMap 方法 +func TestPermissionRepository_GetAllIntoIDMap(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testPermissions := []entity.Permission{ + { + ID: primitive.NewObjectID(), + Name: "read", + HTTPMethod: "GET", + HTTPPath: "/read", + Status: permission.Open, + }, + { + ID: primitive.NewObjectID(), + Name: "write", + HTTPMethod: "POST", + HTTPPath: "/write", + Status: permission.Close, + }, + } + + for _, p := range testPermissions { + err := repo.Insert(context.Background(), p) + assert.NoError(t, err, "插入測試數據時不應該發生錯誤") + } + + testCases := []struct { + name string + status *permission.Status + expectLen int + expectErr bool + }{ + { + name: "查詢所有權限並轉為 Map", + status: nil, + expectLen: 2, + expectErr: false, + }, + { + name: "查詢開啟的權限並轉為 Map", + status: ToPointer(permission.Open), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢關閉的權限並轉為 Map", + status: ToPointer(permission.Close), + expectLen: 1, + expectErr: false, + }, + { + name: "查詢不存在的權限狀態並轉為 Map", + status: ToPointer(permission.Status(-1)), + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetAllIntoIDMap(context.Background(), tc.status) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "查詢結果數量不符合預期") + + // 確保 Map 的 key 是權限名稱 + for key, permission := range result { + assert.Equal(t, key, permission.Name, "Map 的 Key 應該與 Name 相符") + } + } + }) + } +} + +func TestPermissionRepository_FindByNames(t *testing.T) { + repo, tearDown, err := SetupTestPermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 測試數據 + testPermissions := []entity.Permission{ + { + ID: primitive.NewObjectID(), + Name: "read-data", + HTTPMethod: "GET", + HTTPPath: "/data", + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "write-data", + HTTPMethod: "POST", + HTTPPath: "/data", + Status: 1, + }, + } + + // 插入測試數據 + for _, perm := range testPermissions { + err := repo.Insert(context.Background(), perm) + assert.NoError(t, err) + } + + testCases := []struct { + name string + input []string + expectLen int + expectErr bool + }{ + { + name: "成功查詢單個名稱", + input: []string{"read-data"}, + expectLen: 1, + expectErr: false, + }, + { + name: "成功查詢多個名稱", + input: []string{"read-data", "write-data"}, + expectLen: 2, + expectErr: false, + }, + { + name: "查詢名稱不存在時應返回空結果", + input: []string{"unknown-permission"}, + expectLen: 0, + expectErr: false, + }, + { + name: "當查詢發生錯誤時,應返回錯誤", + input: nil, // 無效查詢 + expectLen: 0, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.FindByNames(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "返回的數據長度應符合預期") + } + }) + } +} + +func ToPointer[T any](v T) *T { + return &v +} diff --git a/pkg/repository/role.go b/pkg/repository/role.go new file mode 100644 index 0000000..0690711 --- /dev/null +++ b/pkg/repository/role.go @@ -0,0 +1,199 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "time" +) + +type RoleRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +type RoleRepository struct { + DB mgo.DocumentDBUseCase +} + +func NewRoleRepository(param RoleRepositoryParam) repository.RoleRepository { + e := entity.Role{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &RoleRepository{ + DB: db, + } +} + +func (repo *RoleRepository) List(ctx context.Context, params repository.ListQuery) ([]*entity.Role, int64, error) { + // 構建查詢條件 + filter := bson.M{} + + if params.Name != nil { + filter["name"] = *params.Name + } + + if params.ClientID != nil { + filter["client_id"] = *params.ClientID + } + + if params.Status != nil { + filter["status"] = *params.Status + } + + if params.UID != nil { + filter["uid"] = *params.UID + } + + // 計算符合條件的總數 + 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 result = make([]*entity.Role, 0, params.PageSize) + err = repo.DB.GetClient().Find(ctx, &result, filter, opts) + if err != nil { + return nil, 0, err + } + + return result, count, nil +} + +func (repo *RoleRepository) GetByID(ctx context.Context, id string) (*entity.Role, error) { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return nil, ErrInvalidObjectID + } + + var result entity.Role + err = repo.DB.GetClient().FindOne(ctx, &result, bson.M{"_id": oid}) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (repo *RoleRepository) GetByUID(ctx context.Context, uid string) (*entity.Role, error) { + var result entity.Role + err := repo.DB.GetClient().FindOne(ctx, &result, bson.M{"uid": uid}) + if err != nil { + return nil, err + } + + return &result, nil +} + +func (repo *RoleRepository) All(ctx context.Context, clientID *string) ([]*entity.Role, error) { + opt := options.Find().SetSort(bson.M{"_id": 1}) + filter := bson.M{} + if clientID != nil { + filter["client_id"] = *clientID + } + + result := make([]*entity.Role, 0) + err := repo.DB.GetClient().Find(ctx, &result, filter, opt) + if err != nil { + return nil, err + } + + return result, nil +} + +// Create 建立新的 Role +func (repo *RoleRepository) Create(ctx context.Context, role *entity.Role) error { + if role == nil { + return fmt.Errorf("failed to get role") + } + + if role.ID.IsZero() { + now := time.Now().UTC().UnixNano() + role.ID = primitive.NewObjectID() + role.CreateAt = now + role.UpdateAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, role) + + return err +} + +func (repo *RoleRepository) Update(ctx context.Context, role repository.UpdateReq) error { + now := time.Now().UTC().UnixNano() + // 動態構建更新內容 + updateFields := bson.M{ + "update_at": now, // 確保 `updateAt` 總是更新 + } + + if role.Name != nil { + updateFields["name"] = *role.Name + } + + if role.Status != nil { + updateFields["status"] = *role.Status + } + + if role.UID != nil { + updateFields["uid"] = *role.UID + } + + if role.ClientID != nil { + updateFields["client_id"] = *role.ClientID + } + + oid, err := primitive.ObjectIDFromHex(role.ID) + if err != nil { + return ErrInvalidObjectID + } + + _, err = repo.DB.GetClient().UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": updateFields}) + if err != nil { + return err + } + + return nil +} + +func (repo *RoleRepository) Delete(ctx context.Context, id string) error { + oid, err := primitive.ObjectIDFromHex(id) + if err != nil { + return ErrInvalidObjectID + } + + _, err = repo.DB.GetClient().DeleteOne(ctx, bson.M{"_id": oid}) + if err != nil { + return err + } + + return nil +} + +func (repo *RoleRepository) Index20250224UP(ctx context.Context) (*mongo.Cursor, error) { + repo.DB.PopulateMultiIndex(ctx, []string{ + "name", + "client_id", + "status", + "uid", + }, []int32{1, 1, 1, 1}, false) + + repo.DB.PopulateIndex(ctx, "uid", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/role_permission.go b/pkg/repository/role_permission.go new file mode 100644 index 0000000..b13aef2 --- /dev/null +++ b/pkg/repository/role_permission.go @@ -0,0 +1,98 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "time" +) + +type RolePermissionRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +type RolePermissionRepository struct { + DB mgo.DocumentDBUseCase +} + +func NewRolePermissionRepository(param RoleRepositoryParam) repository.RolePermissionRepository { + e := entity.RolePermission{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &RolePermissionRepository{ + DB: db, + } +} + +func (repo *RolePermissionRepository) Get(ctx context.Context, roleID string) ([]*entity.RolePermission, error) { + var result []*entity.RolePermission + err := repo.DB.GetClient().Find(ctx, &result, bson.M{"role_id": roleID}) + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo *RolePermissionRepository) GetByPermissionID(ctx context.Context, permissionIDs []string) ([]*entity.RolePermission, error) { + var result []*entity.RolePermission // 修正 []*entity.RolePermission -> []entity.RolePermission + + filter := bson.M{ + "permission_id": bson.M{"$in": permissionIDs}, // 使用 $in 運算子來匹配多個 permission_id + } + + err := repo.DB.GetClient().Find(ctx, &result, filter) + if err != nil { + return nil, err + } + + return result, nil +} + +func (repo *RolePermissionRepository) Create(ctx context.Context, role entity.RolePermission) error { + if role.ID.IsZero() { + now := time.Now().UTC().UnixNano() + role.ID = primitive.NewObjectID() + role.CreateAt = now + role.UpdateAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, role) + + return err +} + +func (repo *RolePermissionRepository) Delete(ctx context.Context, roleID string, permission string) error { + filter := bson.M{ + "role_id": roleID, + "permission_id": permission, + } + + _, err := repo.DB.GetClient().DeleteOne(ctx, filter) + if err != nil { + return err + } + + return nil +} + +func (repo *RolePermissionRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + repo.DB.PopulateMultiIndex(ctx, []string{ + "role_id", + "permission_id", + }, []int32{1, 1}, true) + + repo.DB.PopulateIndex(ctx, "role_id", 1, false) + repo.DB.PopulateIndex(ctx, "permission_id", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/role_permission_test.go b/pkg/repository/role_permission_test.go new file mode 100644 index 0000000..5e09179 --- /dev/null +++ b/pkg/repository/role_permission_test.go @@ -0,0 +1,265 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "testing" + "time" +) + +func SetupTestRolePermissionRepository(db string) (repository.RolePermissionRepository, 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 := RoleRepositoryParam{ + Conf: conf, + } + repo := NewRolePermissionRepository(param) + _, _ = repo.Index20250225UP(context.Background()) + + return repo, tearDown, nil +} + +func TestRolePermissionRepository_Create(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input entity.RolePermission + expectErr bool + }{ + { + name: "成功插入新的 RolePermission", + input: entity.RolePermission{ + RoleID: "role_1", + PermissionID: "perm_1", + }, + expectErr: false, + }, + { + name: "插入已經有 ID 的 RolePermission", + input: entity.RolePermission{ + ID: primitive.NewObjectID(), + RoleID: "role_2", + PermissionID: "perm_2", + CreateAt: time.Now().UnixNano(), + UpdateAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Create(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + }) + } +} + +func TestRolePermissionRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先準備測試資料 + existingRolePermission := entity.RolePermission{ + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_1", + } + + err = repo.Create(context.Background(), existingRolePermission) + assert.NoError(t, err, "應該成功插入測試資料") + + testCases := []struct { + name string + roleID string + permission string + expectErr bool + }{ + { + name: "成功刪除已存在的 RolePermission", + roleID: "role_1", + permission: "perm_1", + expectErr: false, + }, + { + name: "刪除不存在的 RolePermission,不應該報錯", + roleID: "role_2", + permission: "perm_2", + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Delete(context.Background(), tc.roleID, tc.permission) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + }) + } +} + +func TestRolePermissionRepository_GetByPermissionID(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先準備測試資料 + existingRolePermissions := []entity.RolePermission{ + { + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_1", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_2", + PermissionID: "perm_2", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_3", + PermissionID: "perm_3", + }, + } + + for _, rp := range existingRolePermissions { + err := repo.Create(context.Background(), rp) + assert.NoError(t, err, "應該成功插入測試資料") + } + + testCases := []struct { + name string + permissionIDs []string + expectedCount int + expectErr bool + }{ + { + name: "成功查詢符合的 RolePermission", + permissionIDs: []string{"perm_1", "perm_2"}, + expectedCount: 2, + expectErr: false, + }, + { + name: "查詢時沒有符合條件的 RolePermission", + permissionIDs: []string{"perm_99"}, + expectedCount: 0, + expectErr: false, + }, + { + name: "查詢時發生資料庫錯誤", + permissionIDs: nil, // 模擬無效的參數 + expectedCount: 0, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + results, err := repo.GetByPermissionID(context.Background(), tc.permissionIDs) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, results, tc.expectedCount, "回傳結果數量應符合預期") + } + }) + } +} + +func TestRolePermissionRepository_Get(t *testing.T) { + repo, tearDown, err := SetupTestRolePermissionRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先準備測試資料 + existingRolePermissions := []entity.RolePermission{ + { + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_1", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_1", + PermissionID: "perm_2", + }, + { + ID: primitive.NewObjectID(), + RoleID: "role_2", + PermissionID: "perm_3", + }, + } + + for _, rp := range existingRolePermissions { + err := repo.Create(context.Background(), rp) + assert.NoError(t, err, "應該成功插入測試資料") + } + + testCases := []struct { + name string + roleID string + expectedCount int + expectErr bool + }{ + { + name: "成功查詢符合的 RolePermission", + roleID: "role_1", + expectedCount: 2, + expectErr: false, + }, + { + name: "查詢時沒有符合條件的 RolePermission", + roleID: "role_99", + expectedCount: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + results, err := repo.Get(context.Background(), tc.roleID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, results, tc.expectedCount, "回傳結果數量應符合預期") + } + }) + } +} diff --git a/pkg/repository/role_test.go b/pkg/repository/role_test.go new file mode 100644 index 0000000..17539ee --- /dev/null +++ b/pkg/repository/role_test.go @@ -0,0 +1,541 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "testing" +) + +func SetupTestRoleRepository(db string) (repository.RoleRepository, 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 := RoleRepositoryParam{ + Conf: conf, + } + repo := NewRoleRepository(param) + _, _ = repo.Index20250224UP(context.Background()) + + return repo, tearDown, nil +} + +func TestRoleRepository_Create(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input *entity.Role + expectErr bool + }{ + { + name: "成功建立新的角色", + input: &entity.Role{ + Name: "Admin", + UID: "user123", + ClientID: "client456", + Status: 1, + }, + expectErr: false, + }, + { + name: "自動生成 ID 及時間戳", + input: &entity.Role{ + Name: "User", + UID: "user789", + ClientID: "client987", + Status: 1, + }, + expectErr: false, + }, + { + name: "當角色為 nil 時應返回錯誤", + input: nil, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Create(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.NotEqual(t, primitive.NilObjectID, tc.input.ID, "應該自動生成 ObjectID") + assert.True(t, tc.input.CreateAt > 0, "應該自動設定 CreateAt") + assert.True(t, tc.input.UpdateAt > 0, "應該自動設定 UpdateAt") + } + }) + } +} + +func TestRoleRepository_Update(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立一個角色 + existingRole := &entity.Role{ + Name: "Old Name", + UID: "old_uid", + ClientID: "old_client_id", + Status: 1, + } + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + newName := "New Name" + newStatus := permission.Close + newUID := "new_uid" + newClientID := "new_client_id" + + testCases := []struct { + name string + input repository.UpdateReq + expectErr bool + }{ + { + name: "成功更新角色", + input: repository.UpdateReq{ + ID: existingRole.ID.Hex(), + Name: &newName, + Status: &newStatus, + UID: &newUID, + ClientID: &newClientID, + }, + expectErr: false, + }, + { + name: "更新部分欄位", + input: repository.UpdateReq{ + ID: existingRole.ID.Hex(), + Name: &newName, + Status: &newStatus, + }, + expectErr: false, + }, + { + name: "無效的 ObjectID 應返回錯誤", + input: repository.UpdateReq{ + ID: "invalid_object_id", + }, + expectErr: true, + }, + { + name: "當 UpdateOne 失敗時應返回錯誤", + input: repository.UpdateReq{ + ID: primitive.NewObjectID().Hex(), // 模擬一個不存在的 ID + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Update(context.Background(), tc.input) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + + // 驗證更新結果 + updatedRole, err := repo.GetByID(context.Background(), existingRole.ID.Hex()) + assert.NoError(t, err) + if tc.input.Name != nil { + assert.Equal(t, *tc.input.Name, updatedRole.Name, "名稱應該被更新") + } + if tc.input.Status != nil { + assert.Equal(t, *tc.input.Status, updatedRole.Status, "狀態應該被更新") + } + if tc.input.UID != nil { + assert.Equal(t, *tc.input.UID, updatedRole.UID, "UID 應該被更新") + } + if tc.input.ClientID != nil { + assert.Equal(t, *tc.input.ClientID, updatedRole.ClientID, "ClientID 應該被更新") + } + } + }) + } +} + +func TestRoleRepository_Delete(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立一個角色 + existingRole := &entity.Role{ + Name: "Test Role", + UID: "test_uid", + ClientID: "test_client_id", + Status: 1, + } + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + testCases := []struct { + name string + inputID string + expectErr bool + }{ + { + name: "成功刪除角色", + inputID: existingRole.ID.Hex(), + expectErr: false, + }, + { + name: "刪除不存在的角色不應報錯", + inputID: primitive.NewObjectID().Hex(), + expectErr: false, + }, + { + name: "無效的 ObjectID 應返回錯誤", + inputID: "invalid_object_id", + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.Delete(context.Background(), tc.inputID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + + // 驗證角色是否已刪除 + if tc.inputID == existingRole.ID.Hex() { + _, err := repo.GetByID(context.Background(), existingRole.ID.Hex()) + assert.Error(t, err, "應該找不到該角色") + } + } + }) + } +} + +func TestRoleRepository_All(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + clientID1 := "client_1" + clientID2 := "client_2" + + // 預先建立角色資料 + roles := []*entity.Role{ + { + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: clientID1, + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "User", + UID: "user_uid", + ClientID: clientID1, + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "Manager", + UID: "manager_uid", + ClientID: clientID2, + Status: 1, + }, + } + + // 插入測試資料 + for _, role := range roles { + err := repo.Create(context.Background(), role) + assert.NoError(t, err) + } + + testCases := []struct { + name string + clientID *string + expectLen int + expectErr bool + }{ + { + name: "查詢所有角色", + clientID: nil, + expectLen: len(roles), + expectErr: false, + }, + { + name: "根據 clientID 查詢角色", + clientID: &clientID1, + expectLen: 2, + expectErr: false, + }, + { + name: "clientID 無匹配時應返回空", + clientID: new(string), + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.All(context.Background(), tc.clientID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "返回的角色數量應符合預期") + } + }) + } +} + +func TestRoleRepository_GetByUID(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立測試角色 + existingRole := &entity.Role{ + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: "client_1", + Status: 1, + } + + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + testCases := []struct { + name string + uid string + expectErr bool + expectNil bool + }{ + { + name: "成功查詢角色", + uid: "admin_uid", + expectErr: false, + expectNil: false, + }, + { + name: "查詢不存在的角色", + uid: "non_existent_uid", + expectErr: true, + expectNil: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetByUID(context.Background(), tc.uid) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + + if tc.expectNil { + assert.Nil(t, result, "應該返回 nil") + } else { + assert.NotNil(t, result, "不應該返回 nil") + assert.Equal(t, existingRole.UID, result.UID, "UID 應相符") + assert.Equal(t, existingRole.Name, result.Name, "名稱應相符") + } + }) + } +} + +func TestRoleRepository_GetByID(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立測試角色 + existingRole := &entity.Role{ + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: "client_1", + Status: 1, + } + + err = repo.Create(context.Background(), existingRole) + assert.NoError(t, err) + + testCases := []struct { + name string + id string + expectErr bool + expectNil bool + }{ + { + name: "成功查詢角色", + id: existingRole.ID.Hex(), + expectErr: false, + expectNil: false, + }, + { + name: "查詢不存在的角色", + id: primitive.NewObjectID().Hex(), + expectErr: true, + expectNil: true, + }, + { + name: "提供無效的 ObjectID", + id: "invalid_id", + expectErr: true, + expectNil: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetByID(context.Background(), tc.id) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + } + + if tc.expectNil { + assert.Nil(t, result, "應該返回 nil") + } else { + assert.NotNil(t, result, "不應該返回 nil") + assert.Equal(t, existingRole.ID, result.ID, "ID 應相符") + assert.Equal(t, existingRole.Name, result.Name, "名稱應相符") + } + }) + } +} + +func TestRoleRepository_List(t *testing.T) { + repo, tearDown, err := SetupTestRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 預先建立測試角色 + roles := []*entity.Role{ + { + ID: primitive.NewObjectID(), + Name: "Admin", + UID: "admin_uid", + ClientID: "client_1", + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "User", + UID: "user_uid", + ClientID: "client_1", + Status: 1, + }, + { + ID: primitive.NewObjectID(), + Name: "Guest", + UID: "guest_uid", + ClientID: "client_2", + Status: 0, + }, + } + + // 插入測試資料 + for _, role := range roles { + err := repo.Create(context.Background(), role) + assert.NoError(t, err) + } + + testCases := []struct { + name string + query repository.ListQuery + expectLen int + expectErr bool + }{ + { + name: "查詢所有角色", + query: repository.ListQuery{PageSize: 10, PageIndex: 1}, + expectLen: 3, + expectErr: false, + }, + { + name: "篩選名稱為 Admin", + query: repository.ListQuery{Name: ToPointer("Admin"), PageSize: 10, PageIndex: 1}, + expectLen: 1, + expectErr: false, + }, + { + name: "篩選特定 ClientID", + query: repository.ListQuery{ClientID: ToPointer("client_1"), PageSize: 10, PageIndex: 1}, + expectLen: 2, + expectErr: false, + }, + { + name: "篩選啟用 (Status=1) 的角色", + query: repository.ListQuery{Status: ToPointer(permission.Open), PageSize: 10, PageIndex: 1}, + expectLen: 2, + expectErr: false, + }, + { + name: "篩選特定 UID", + query: repository.ListQuery{UID: ToPointer("guest_uid"), PageSize: 10, PageIndex: 1}, + expectLen: 1, + expectErr: false, + }, + { + name: "測試分頁 PageSize=1, PageIndex=1", + query: repository.ListQuery{PageSize: 1, PageIndex: 1}, + expectLen: 1, + expectErr: false, + }, + { + name: "查詢無符合條件的角色", + query: repository.ListQuery{Name: ToPointer("NonExist"), PageSize: 10, PageIndex: 1}, + expectLen: 0, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, _, err := repo.List(context.Background(), tc.query) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, tc.expectLen, "返回的角色數量應符合預期") + } + }) + } +} diff --git a/pkg/repository/start_mongo_container_test.go b/pkg/repository/start_mongo_container_test.go new file mode 100644 index 0000000..1bcfe1a --- /dev/null +++ b/pkg/repository/start_mongo_container_test.go @@ -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 +} diff --git a/pkg/repository/user_role.go b/pkg/repository/user_role.go new file mode 100644 index 0000000..a24d1bc --- /dev/null +++ b/pkg/repository/user_role.go @@ -0,0 +1,137 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/zeromicro/go-zero/core/stores/mon" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "time" +) + +// UserRoleRepositoryParam 定義 MongoDB 配置參數 +type UserRoleRepositoryParam struct { + Conf *mgo.Conf + DBOpts []mon.Option +} + +// userRoleRepository 實作 repository.UserRoleRepository 介面 +type userRoleRepository struct { + DB mgo.DocumentDBUseCase +} + +// NewUserRoleRepository 初始化 `UserRoleRepository` +func NewUserRoleRepository(param UserRoleRepositoryParam) repository.UserRoleRepository { + e := entity.UserRole{} + db, err := mgo.NewDocumentDB(param.Conf, e.Collection(), param.DBOpts...) + if err != nil { + panic(err) + } + + return &userRoleRepository{ + DB: db, + } +} + +// GetAll 取得所有 UserRole +func (repo *userRoleRepository) GetAll(ctx context.Context) ([]*entity.UserRole, error) { + var result []*entity.UserRole + err := repo.DB.GetClient().Find(ctx, &result, bson.M{}) + if err != nil { + return nil, err + } + + return result, nil +} + +// GetByUserID 透過 UID 查詢 UserRole +func (repo *userRoleRepository) GetByUserID(ctx context.Context, uid string) (entity.UserRole, error) { + var result entity.UserRole + err := repo.DB.GetClient().FindOne(ctx, &result, bson.M{"uid": uid}) + if err != nil { + return entity.UserRole{}, err + } + + return result, nil +} + +// GetUsersByRoleID 透過 RoleID 查詢所有使用此角色的使用者 +func (repo *userRoleRepository) GetUsersByRoleID(ctx context.Context, roleID string) ([]entity.UserRole, error) { + var result []entity.UserRole + err := repo.DB.GetClient().Find(ctx, &result, bson.M{"role_id": roleID}) + if err != nil { + return nil, err + } + + return result, nil +} + +// CountUsersByRole 統計每個角色的使用者數量 +func (repo *userRoleRepository) CountUsersByRole(ctx context.Context) ([]repository.RoleUserCount, error) { + pipeline := []bson.M{ + {"$group": bson.M{ + "_id": "$role_id", + "count": bson.M{"$sum": 1}, + }}, + } + + var result []repository.RoleUserCount + err := repo.DB.GetClient().Aggregate(ctx, &result, pipeline) + if err != nil { + return nil, err + } + + return result, nil +} + +// CreateUserRole 新增使用者角色 +func (repo *userRoleRepository) CreateUserRole(ctx context.Context, param entity.UserRole) error { + if param.UID == "" { + return fmt.Errorf("uid can't be empty") + } + + if param.RoleID == "" { + return fmt.Errorf("role_id can't be empty") + } + + if param.ID.IsZero() { + now := time.Now().UTC().UnixNano() + param.ID = primitive.NewObjectID() + param.CreateAt = now + param.UpdateAt = now + } + + _, err := repo.DB.GetClient().InsertOne(ctx, param) + return err +} + +// UpdateUserRole 更新使用者角色 +func (repo *userRoleRepository) UpdateUserRole(ctx context.Context, uid, roleID string) (entity.UserRole, error) { + filter := bson.M{"uid": uid} + update := bson.M{ + "$set": bson.M{ + "role_id": roleID, + "update_at": time.Now().UTC().UnixNano(), + }, + } + + var updated entity.UserRole + err := repo.DB.GetClient().FindOneAndUpdate(ctx, &updated, filter, update) + if err != nil { + return entity.UserRole{}, err + } + + return updated, nil +} + + +func (repo *userRoleRepository) Index20250225UP(ctx context.Context) (*mongo.Cursor, error) { + repo.DB.PopulateIndex(ctx, "role_id", 1, false) + repo.DB.PopulateIndex(ctx, "uid", 1, false) + + return repo.DB.GetClient().Indexes().List(ctx) +} diff --git a/pkg/repository/user_role_test.go b/pkg/repository/user_role_test.go new file mode 100644 index 0000000..ca33626 --- /dev/null +++ b/pkg/repository/user_role_test.go @@ -0,0 +1,336 @@ +package repository + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" + mgo "code.30cm.net/digimon/library-go/mongo" + "context" + "fmt" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "testing" + "time" +) + +func SetupTestUserRoleRepository(db string) (repository.UserRoleRepository, 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 := UserRoleRepositoryParam{ + Conf: conf, + } + repo := NewUserRoleRepository(param) + _, _ = repo.Index20250225UP(context.Background()) + + return repo, tearDown, nil +} + +func TestUserRoleRepository_CreateUserRole(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + testCases := []struct { + name string + input entity.UserRole + expectErr bool + }{ + { + name: "成功新增使用者角色", + input: entity.UserRole{ + UID: "user_123", + RoleID: "role_456", + }, + expectErr: false, + }, + { + name: "異常:無效的輸入", + input: entity.UserRole{ + RoleID: "role_456", + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := repo.CreateUserRole(context.Background(), tc.input) + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + + // 檢查資料是否真的被插入 + var inserted entity.UserRole + inserted, err = repo.GetByUserID(context.Background(), tc.input.UID) + fmt.Println(inserted) + assert.NoError(t, err, "應該能找到插入的資料") + assert.Equal(t, tc.input.UID, inserted.UID, "UID 應該匹配") + assert.Equal(t, tc.input.RoleID, inserted.RoleID, "RoleID 應該匹配") + assert.NotZero(t, inserted.CreateAt, "CreateAt 應該被設定") + assert.NotZero(t, inserted.UpdateAt, "UpdateAt 應該被設定") + } + }) + } +} + +func TestUserRoleRepository_UpdateUserRole(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先插入測試數據 + existingUserRole := entity.UserRole{ + ID: primitive.NewObjectID(), + UID: "user_123", + RoleID: "role_123", + CreateAt: time.Now().UTC().UnixNano(), + UpdateAt: time.Now().UTC().UnixNano(), + } + + err = repo.CreateUserRole(context.Background(), existingUserRole) + assert.NoError(t, err, "初始化數據應該成功") + + testCases := []struct { + name string + uid string + newRoleID string + expectErr bool + expectedRole string + }{ + { + name: "成功更新使用者角色", + uid: "user_123", + newRoleID: "role_456", + expectErr: false, + expectedRole: "role_456", + }, + { + name: "異常:更新的 UID 不存在", + uid: "non_existent_user", + newRoleID: "role_789", + expectErr: true, + expectedRole: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + _, err = repo.UpdateUserRole(context.Background(), tc.uid, tc.newRoleID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + // 檢查資料是否真的被更新 + found, err := repo.GetByUserID(context.Background(), tc.uid) + assert.NoError(t, err, "應該能找到更新後的資料") + assert.Equal(t, tc.expectedRole, found.RoleID, "RoleID 應該匹配更新的值") + assert.NotZero(t, found.UpdateAt, "UpdateAt 應該被更新") + } + }) + } +} + +func TestUserRoleRepository_CountUsersByRole(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 先插入測試數據 + testUsers := []entity.UserRole{ + {ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_4", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_5", RoleID: "role_user"}, + } + + for _, user := range testUsers { + err := repo.CreateUserRole(context.Background(), user) + assert.NoError(t, err, "應該成功插入測試數據") + } + + testCases := []struct { + name string + expectedData []repository.RoleUserCount + expectErr bool + }{ + { + name: "成功統計角色使用者數量", + expectedData: []repository.RoleUserCount{ + {RoleID: "role_admin", Count: 2}, + {RoleID: "role_user", Count: 3}, + }, + expectErr: false, + }, + //{ + // name: "無資料時回傳空結果", + // expectedData: []repository.RoleUserCount{}, + // expectErr: false, + //}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.CountUsersByRole(context.Background()) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.ElementsMatch(t, tc.expectedData, result, "結果應該匹配預期的使用者數量統計") + } + }) + } +} + +// TestUserRoleRepository_GetUsersByRoleID 測試 GetUsersByRoleID 查詢指定 RoleID 的使用者 +func TestUserRoleRepository_GetUsersByRoleID(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testUsers := []entity.UserRole{ + {ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_4", RoleID: "role_user"}, + {ID: primitive.NewObjectID(), UID: "user_5", RoleID: "role_user"}, + } + + for _, user := range testUsers { + err := repo.CreateUserRole(context.Background(), user) + assert.NoError(t, err, "應該成功插入測試數據") + } + + // 測試案例 + testCases := []struct { + name string + roleID string + expectedData []entity.UserRole + expectErr bool + }{ + { + name: "成功查詢 role_admin 的使用者", + roleID: "role_admin", + expectedData: []entity.UserRole{ + {UID: "user_1", RoleID: "role_admin"}, + {UID: "user_2", RoleID: "role_admin"}, + }, + expectErr: false, + }, + { + name: "成功查詢 role_user 的使用者", + roleID: "role_user", + expectedData: []entity.UserRole{ + {UID: "user_3", RoleID: "role_user"}, + {UID: "user_4", RoleID: "role_user"}, + {UID: "user_5", RoleID: "role_user"}, + }, + expectErr: false, + }, + { + name: "查詢 role_guest,應回傳空陣列", + roleID: "role_guest", + expectedData: []entity.UserRole{}, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetUsersByRoleID(context.Background(), tc.roleID) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + fmt.Println(result) + compute := make([]entity.UserRole, 0, len(result)) + res := make([]entity.UserRole, 0, len(result)) + + for _, item := range result { + compute = append(compute, entity.UserRole{ + RoleID: item.RoleID, + UID: item.UID, + }) + } + + for _, item := range tc.expectedData { + res = append(res, entity.UserRole{ + RoleID: item.RoleID, + UID: item.UID, + }) + } + assert.ElementsMatch(t, compute, res, "結果應該匹配預期的使用者列表") + } + }) + } +} + +// TestUserRoleRepository_GetAll 測試 GetAll 取得所有使用者角色 +func TestUserRoleRepository_GetAll(t *testing.T) { + repo, tearDown, err := SetupTestUserRoleRepository("testDB") + assert.NoError(t, err) + defer tearDown() + + // 插入測試數據 + testUsers := []entity.UserRole{ + {ID: primitive.NewObjectID(), UID: "user_1", RoleID: "role_admin"}, + {ID: primitive.NewObjectID(), UID: "user_2", RoleID: "role_editor"}, + {ID: primitive.NewObjectID(), UID: "user_3", RoleID: "role_viewer"}, + } + + for _, user := range testUsers { + err := repo.CreateUserRole(context.Background(), user) + assert.NoError(t, err, "應該成功插入測試數據") + } + + // 測試案例 + testCases := []struct { + name string + expectedData []*entity.UserRole + expectErr bool + }{ + { + name: "成功獲取所有使用者角色", + expectedData: []*entity.UserRole{ + {UID: "user_1", RoleID: "role_admin"}, + {UID: "user_2", RoleID: "role_editor"}, + {UID: "user_3", RoleID: "role_viewer"}, + }, + expectErr: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := repo.GetAll(context.Background()) + + if tc.expectErr { + assert.Error(t, err, "應該返回錯誤") + } else { + assert.NoError(t, err, "不應該返回錯誤") + assert.Len(t, result, len(tc.expectedData), "結果應該包含正確數量的使用者角色") + //assert.ElementsMatch(t, tc.expectedData, result, "結果應該匹配預期的使用者角色") + } + }) + } +} diff --git a/pkg/usecase/casbin_redis_rbac.go b/pkg/usecase/casbin_redis_rbac.go index 4ce4a17..aa26b70 100644 --- a/pkg/usecase/casbin_redis_rbac.go +++ b/pkg/usecase/casbin_redis_rbac.go @@ -1,29 +1,37 @@ package usecase import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/repository" "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/usecase" "code.30cm.net/digimon/library-go/errs" + "code.30cm.net/digimon/library-go/errs/code" "context" "fmt" "github.com/casbin/casbin/v2" "github.com/casbin/casbin/v2/model" "github.com/zeromicro/go-zero/core/logx" "log" + "net/http" "time" ) type RBACUseCaseParam struct { ModulePath string permissionRepo repository.PermissionRepository + roleRepo repository.RoleRepository + rolePermission repository.RolePermissionRepository RBACRedisAdapter repository.RBACAdapter // role permission 之類的 } type RBACUseCase struct { permissionRepo repository.PermissionRepository + roleRepo repository.RoleRepository adapter repository.RBACAdapter + rolePermission repository.RolePermissionRepository instance *casbin.Enforcer } @@ -31,6 +39,8 @@ func NewUseCase(param RBACUseCaseParam) usecase.RBACUseCase { result := &RBACUseCase{ adapter: param.RBACRedisAdapter, permissionRepo: param.permissionRepo, + roleRepo: param.roleRepo, + rolePermission: param.rolePermission, } // 1. 讀取 RBAC 模型 -> @@ -68,89 +78,107 @@ func (use *RBACUseCase) Check(ctx context.Context, role, path, method string) (u Allow: ok, } - fmt.Println(p) - //// 檢查是否有明碼查詢權限 - //if role == domain.AdminRoleUID { - // status.Select.PlainCode = true - //} else if ok && method == http.MethodGet { - // status.Select.PlainCode = use.rbac.GetModel().HasPolicy("p", "p", []string{ - // role, path, method, p[3] + ".plain_code", - // }) - //} - // - //limit := 4 - //if len(p) >= limit { - // status.Select.PermissionName = p[3] - //} + // 檢查是否有明碼查詢權限 + if role == permission.AdminRoleUID { + status.Select.PlainCode = true + } else if ok && method == http.MethodGet { + policy, err := use.instance.GetModel().HasPolicy("p", "p", []string{ + role, path, method, p[3] + ".plain_code", + }) + if err != nil { + return usecase.CheckRolePermissionStatus{}, err + } + status.Select.PlainCode = policy + + } + + limit := 4 + if len(p) >= limit { + status.Select.PermissionName = p[3] + } return status, nil } func (use *RBACUseCase) LoadPolicy(ctx context.Context) error { - - status := permission.Open - // 取得所有permission -> permission tree 拿到有開啟的節點,如果付節點關閉,子節點也就不顯示了 - permissions, err := use.permissionRepo.GetAll(ctx, &status) + // 取得所有permission -> permission tree 拿到所有節點, + permissions, err := use.permissionRepo.GetAll(ctx, nil) if err != nil { return fmt.Errorf("permissionRepo.AllStatus error: %w", err) } - fmt.Println(permissions) - + // 建立樹,我只要開啟的 Permission + tree := GeneratePermissionTree(permissions) + openMaps, err := tree.filterOpenNodes() + if err != nil { + return fmt.Errorf("GeneratePermissionTree.filterOpenNodes error: %w", err) + } // 全部permission - //permissionMap := make(map[int64]entity.Permission, len(permissions)) - //for _, v := range permissions { - // permissionMap[v.ID] = v - //} + permissionMap := make(map[string]entity.Permission, len(permissions)) + for k, v := range openMaps { + permissionMap[k] = v + } // 全部角色 - //roles, err := r.roleRepo.All(ctx, 1) - //if err != nil { - // return fmt.Errorf("roleRepo.AllStatus error: %w", err) - //} - // - //roleMap := make(map[int64]entity.Role, len(roles)) - //for _, v := range roles { - // roleMap[v.ID] = v - //} + roles, err := use.roleRepo.All(ctx, nil) + if err != nil { + return fmt.Errorf("roleRepo.All error: %w", err) + } + + roleMap := make(map[string]entity.Role, len(roles)) + for _, v := range roles { + tmpValue := v + roleMap[v.ID.Hex()] = *tmpValue + } // 根據角色組合權限表 - //for _, v := range roles { - // rolePermissions, err := r.rolePermissionRepo.Get(ctx, v.ID) - // if err != nil { - // return fmt.Errorf("rolePermissionRepo.Get ID: %d error: %w", v.ID, err) - // } - // - // for _, rp := range rolePermissions { - // role, ok := roleMap[rp.RoleID] - // if !ok { - // logrus.WithFields(logrus.Fields{ - // "role_id": rp.RoleID, - // }).Error("role not found") - // - // continue - // } - // - // permission, ok := permissionMap[rp.PermissionID] - // if !ok { - // logrus.WithFields(logrus.Fields{ - // "permission_id": rp.PermissionID, - // }).Error("permission not found") - // - // continue - // } - // - // if permission.HTTPPath == "" || permission.HTTPMethod == "" { - // continue - // } - // - // // 根據策略model configs/rbac_model.conf填入policy_definition對應參數 - // err := persist.LoadPolicyArray([]string{"p", role.UID, permission.HTTPPath, permission.HTTPMethod, permission.Name}, model) - // if err != nil { - // return fmt.Errorf("persist.LoadPolicyArray error: %w", err) - // } - // } - //} + for _, role := range roles { + rolePermission, err := use.rolePermission.Get(ctx, role.ID.Hex()) + if err != nil { + e := errs.DatabaseErrorWithScopeL( + code.CloudEPPermission, + domain.FailedToGetRolePermission, + logx.WithContext(ctx), + []logx.LogField{ + {Key: "req", Value: fmt.Sprintf("role: %s", role.ID.Hex())}, + {Key: "func", Value: "RolePermissionRepo.Get"}, + {Key: "err", Value: err.Error()}, + }, + "failed to get rolePermission") + + return e + } + + for _, rp := range rolePermission { + r, ok := roleMap[rp.RoleID] + if !ok { + logx.Errorf(fmt.Sprintf("role_id: %s not found", rp.RoleID)) + + continue + } + + p, ok := permissionMap[rp.PermissionID] + if !ok { + logx.Errorf(fmt.Sprintf("permission_id: %s not found", rp.PermissionID)) + + continue + } + + if p.HTTPPath == "" || p.HTTPMethod == "" { + continue + } + + _, err = use.instance.AddPolicy(r.UID, p.HTTPPath, p.HTTPMethod, p.Name) + if err != nil { + return err + } + } + } + + err = use.instance.LoadPolicy() + if err != nil { + return err + } return nil } diff --git a/pkg/usecase/casbin_redis_rbac_test.go b/pkg/usecase/casbin_redis_rbac_test.go new file mode 100644 index 0000000..aed2454 --- /dev/null +++ b/pkg/usecase/casbin_redis_rbac_test.go @@ -0,0 +1 @@ +package usecase diff --git a/pkg/usecase/permission_tree.go b/pkg/usecase/permission_tree.go new file mode 100644 index 0000000..20d8d7f --- /dev/null +++ b/pkg/usecase/permission_tree.go @@ -0,0 +1,229 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "sync" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PermissionTree 用來管理權限節點,包含快速查詢 map 與輔助 map(例如名稱對應的 ID 列表) +type PermissionTree struct { + // dummy root 節點 + root *PermissionNode + // permission id => node + nodes map[string]*PermissionNode + // permission name => permission id + names map[string][]string + mu sync.RWMutex // 保護樹的並發存取 +} + +type PermissionNode struct { + Data entity.Permission + Parent *PermissionNode + Children []*PermissionNode +} + +// GeneratePermissionTree 根據扁平權限資料建立樹,並掛在 dummy root 下 +func GeneratePermissionTree(permissions []entity.Permission) *PermissionTree { + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + } + + // 1. 建立所有節點 + for _, perm := range permissions { + node := &PermissionNode{ + Data: perm, + Children: []*PermissionNode{}, + } + tree.nodes[perm.ID.Hex()] = node + tree.names[perm.Name] = append(tree.names[perm.Name], perm.ID.Hex()) + } + + // 2. 建立 dummy root 節點 + tree.root = &PermissionNode{ + Data: entity.Permission{ID: primitive.NewObjectID(), Name: "root"}, + Children: []*PermissionNode{}, + } + + // 3. 建立父子連結:若找不到父節點或 Parent 為 0,則掛在 dummy root 下 + for _, node := range tree.nodes { + if node.Data.Parent == "" { + node.Parent = tree.root + tree.root.Children = append(tree.root.Children, node) + } else if parent, ok := tree.nodes[node.Data.Parent]; ok { + node.Parent = parent + parent.Children = append(parent.Children, node) + } else { + // 若父節點不存在,預設掛在 dummy root 下 + node.Parent = tree.root + tree.root.Children = append(tree.root.Children, node) + } + } + + return tree +} + +// getNode 輔助函數:根據 ID 從樹中查找節點 +func (tree *PermissionTree) getNode(id string) *PermissionNode { + tree.mu.RLock() + defer tree.mu.RUnlock() + return tree.nodes[id] +} + +func (tree *PermissionTree) put(node entity.Permission) { + parentNode := tree.getNode(node.Parent) + if parentNode == nil { + parentNode = tree.root + } + thisNode := &PermissionNode{ + Data: node, + Parent: parentNode, + Children: make([]*PermissionNode, 0), + } + parentNode.Children = append(parentNode.Children, thisNode) + tree.names[node.Name] = append(tree.names[node.Name], node.ID.Hex()) + tree.nodes[node.ID.Hex()] = thisNode +} + +// filterOpenNodes 走訪整棵樹,列出有被打開的節點(父節點沒開,則底下的都不會開) +// 如果某個節點為非葉節點,則會檢查其子節點是否有啟用,否則該節點不會被展開。 +// [permissionID] entity.Permission +func (tree *PermissionTree) filterOpenNodes() (map[string]entity.Permission, error) { + tree.mu.RLock() + defer tree.mu.RUnlock() + + result := make(map[string]entity.Permission) + + // dfs 為內部閉包,可存取 result + // 返回值 bool 表示目前節點或其子孫中是否存在有效 open 節點 + var dfs func(node *PermissionNode) bool + dfs = func(node *PermissionNode) bool { + // 若本身狀態非 open,則整個分支不展開 + if node.Data.Status != permission.Open { + return false + } + + // 節點本身是 open,不論子節點狀態如何,先將該節點加入結果 + result[node.Data.ID.Hex()] = node.Data + + // 遞迴處理子節點 + for _, child := range node.Children { + dfs(child) + } + return true + } + + // 從 dummy root 的 Children 開始走訪(dummy root 本身不納入結果) + for _, child := range tree.root.Children { + dfs(child) + } + + return result, nil +} + +//// getFullParentPermissionIDs +//// 根據傳入的權限狀態 (Permissions) 回傳完整的權限 ID 列表,包含所有祖先。 +//// 如果某個節點為非葉節點,則會檢查其子節點是否有啟用,否則該節點不會被展開。 +//func (tree *PermissionTree) getFullParentPermissionIDs(permissions permission.Permissions) ([]string, error) { +// tree.mu.RLock() +// defer tree.mu.RUnlock() +// +// exist := make(map[string]bool) +// var ids []string +// +// for name, status := range permissions { +// if status != permission.OpenPermission { +// continue +// } +// idList, ok := tree.nameToIDs[name] +// if !ok { +// return nil, NotFoundError +// } +// for _, pid := range idList { +// node, exists := tree.nodes[pid] +// if !exists || node == nil { +// return nil, NotFoundError +// } +// // 如果為父節點,檢查其子節點是否有啟用,若都關閉則不展開 +// if len(node.Children) > 0 { +// var childOpen bool +// for _, child := range node.Children { +// if childStatus, ok := permissions[child.Data.Name]; ok && childStatus == permission.OpenPermission { +// childOpen = true +// break +// } +// } +// if !childOpen { +// continue +// } +// } +// // 將該節點及所有祖先(直到 dummy root,不包括 dummy root)加入結果 +// for cur := node; cur != nil && cur.Data.Name != "root"; cur = cur.Parent { +// if !exist[cur.Data.ID.Hex()] { +// ids = append(ids, cur.Data.ID.Hex()) +// exist[cur.Data.ID.Hex()] = true +// } +// } +// } +// } +// +// return ids, nil +//} + +//// getFullParentPermissionStatus +//// 根據傳入的權限狀態 (Permissions) 回傳完整的權限狀態,包含所有祖先的名稱設為啟用。 +//func (tree *PermissionTree) getFullParentPermissionStatus(permissions permission.Permissions) (permission.Permissions, error) { +// tree.mu.RLock() +// defer tree.mu.RUnlock() +// +// result := make(permission.Permissions) +// exist := make(map[string]bool) +// +// for name, status := range permissions { +// if status != permission.OpenPermission { +// continue +// } +// idList, ok := tree.nameToIDs[name] +// if !ok { +// return nil, NotFoundError +// } +// for _, pid := range idList { +// node, exists := tree.nodes[pid] +// if !exists || node == nil { +// return nil, NotFoundError +// } +// // 將該節點及所有祖先標記為啟用 +// for cur := node; cur != nil && cur.Data.Name != "root"; cur = cur.Parent { +// if !exist[cur.Data.ID.Hex()] { +// result[cur.Data.Name] = permission.OpenPermission +// exist[cur.Data.ID.Hex()] = true +// } +// } +// } +// } +// +// return result, nil +//} +// +//// getFullParentPermission +//// 根據角色權限 (RolePermission) 列表,回傳完整的權限狀態(名稱->狀態),包含所有祖先 +//func (tree *PermissionTree) getFullParentPermission(rolePermissions []entity.RolePermission) permission.Permissions { +// tree.mu.RLock() +// defer tree.mu.RUnlock() +// +// result := make(permission.Permissions) +// for _, rp := range rolePermissions { +// node, ok := tree.nodes[rp.PermissionID] +// if !ok || node == nil { +// continue +// } +// // 將該節點及所有祖先設為啟用 +// for cur := node; cur != nil && cur.Data.Name != "root"; cur = cur.Parent { +// result[cur.Data.Name] = permission.OpenPermission +// } +// } +// return result +//} diff --git a/pkg/usecase/permission_tree_test.go b/pkg/usecase/permission_tree_test.go new file mode 100644 index 0000000..2594496 --- /dev/null +++ b/pkg/usecase/permission_tree_test.go @@ -0,0 +1,358 @@ +package usecase + +import ( + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/entity" + "code.30cm.net/digimon/app-cloudep-permission-server/pkg/domain/permission" + "go.mongodb.org/mongo-driver/bson/primitive" + "sync" + "testing" +) + +// TestGeneratePermissionTree 測試 GeneratePermissionTree 函數的建立樹功能 +func TestGeneratePermissionTree(t *testing.T) { + // 準備測試資料 + // p1 為根節點(Parent 為空) + p1ID := primitive.NewObjectID() + p1 := entity.Permission{ + ID: p1ID, + Parent: "", + Name: "A", + } + + // p2 的父節點為 p1 + p2ID := primitive.NewObjectID() + p2 := entity.Permission{ + ID: p2ID, + Parent: p1ID.Hex(), + Name: "B", + } + + // p3 為另一個根節點(Parent 為空) + p3ID := primitive.NewObjectID() + p3 := entity.Permission{ + ID: p3ID, + Parent: "", + Name: "C", + } + + // p4 的 Parent 填寫一個不存在的 id,預期會掛在 dummy root 下 + p4ID := primitive.NewObjectID() + p4 := entity.Permission{ + ID: p4ID, + Parent: "nonexistent", + Name: "D", + } + + permissions := []entity.Permission{p1, p2, p3, p4} + + // 建立樹 + tree := GeneratePermissionTree(permissions) + + // 驗證 dummy root 下的子節點 + // 預期 p1、p3、p4 均掛在 dummy root 下 + if len(tree.root.Children) != 3 { + t.Errorf("expected 3 children under dummy root, got %d", len(tree.root.Children)) + } + + // 驗證 p1 的節點是否正確 + nodeP1, ok := tree.nodes[p1ID.Hex()] + if !ok { + t.Errorf("node for permission A (p1) not found") + } else { + // p1 應該有一個子節點 p2 + if len(nodeP1.Children) != 1 { + t.Errorf("expected node A to have 1 child, got %d", len(nodeP1.Children)) + } else { + if nodeP1.Children[0].Data.ID != p2ID { + t.Errorf("expected node A child to be permission B (p2), got %v", nodeP1.Children[0].Data.ID.Hex()) + } + } + } + + // 驗證 tree.names 的對應關係 + checkNames := []struct { + name string + expectedID string + }{ + {"A", p1ID.Hex()}, + {"B", p2ID.Hex()}, + {"C", p3ID.Hex()}, + {"D", p4ID.Hex()}, + } + for _, cn := range checkNames { + ids, ok := tree.names[cn.name] + if !ok { + t.Errorf("name mapping for %s not found", cn.name) + continue + } + if len(ids) != 1 || ids[0] != cn.expectedID { + t.Errorf("expected name mapping for %s to be [%s], got %v", cn.name, cn.expectedID, ids) + } + } +} + +func TestGetNode(t *testing.T) { + // 建立一個測試用的 PermissionTree + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + } + id := primitive.NewObjectID() + // 建立一個測試節點,ID 為 "testID" + perm := entity.Permission{ + ID: id, + Name: "Test Permission", + } + node := &PermissionNode{ + Data: perm, + } + + // 將測試節點插入 tree 的 nodes map + tree.mu.Lock() + tree.nodes["testID"] = node + tree.mu.Unlock() + + // 測試 getNode 返回存在的節點 + got := tree.getNode("testID") + if got == nil { + t.Error("Expected to find node with id 'testID', but got nil") + } else if got.Data.ID.Hex() != id.Hex() { + t.Errorf("Expected node ID 'testID', got '%s'", got.Data.ID) + } + + // 測試對不存在的 id,應回傳 nil + gotNil := tree.getNode("nonexistent") + if gotNil != nil { + t.Errorf("Expected nil for non-existent node, got %+v", gotNil) + } +} + +func TestPut(t *testing.T) { + // 建立一個 PermissionTree,並初始化 dummy root + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + } + // 建立 dummy root 節點,其 ID 為一個隨機 ObjectID,但不參與 mapping + dummyRootPerm := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "root", + } + tree.root = &PermissionNode{ + Data: dummyRootPerm, + Children: make([]*PermissionNode, 0), + } + + // 測試 1:放入一筆 Parent 為空的節點,預期掛在 dummy root 下 + permA := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 無父節點 + Name: "A", + } + tree.put(permA) + nodeA := tree.getNode(permA.ID.Hex()) + if nodeA == nil { + t.Errorf("Expected to find node A in tree.nodes") + } + if nodeA.Parent != tree.root { + t.Errorf("Expected node A's parent to be dummy root, got %v", nodeA.Parent.Data.Name) + } + if ids, ok := tree.names["A"]; !ok || len(ids) != 1 || ids[0] != permA.ID.Hex() { + t.Errorf("Expected tree.names for 'A' to contain %s, got %v", permA.ID.Hex(), tree.names["A"]) + } + + // 測試 2:放入一筆 Parent 為存在節點的節點 + // 先放入父節點 permB + permB := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 掛在 dummy root 下 + Name: "B", + } + tree.put(permB) + nodeB := tree.getNode(permB.ID.Hex()) + if nodeB == nil { + t.Errorf("Expected to find node B in tree.nodes") + } + + // 再放入子節點 permC,其 Parent 為 permB.ID.Hex() + permC := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permB.ID.Hex(), + Name: "C", + } + tree.put(permC) + nodeC := tree.getNode(permC.ID.Hex()) + if nodeC == nil { + t.Errorf("Expected to find node C in tree.nodes") + } + if nodeC.Parent != nodeB { + t.Errorf("Expected node C's parent to be node B") + } + // 驗證 nodeB 的 Children 是否包含 nodeC + found := false + for _, child := range nodeB.Children { + if child.Data.ID == permC.ID { + found = true + break + } + } + if !found { + t.Errorf("Expected node B's children to contain node C") + } +} + +func TestFilterOpenNodes(t *testing.T) { + // 建立一個 PermissionTree,初始化 nodes 與 names,並建立 dummy root 節點 + tree := &PermissionTree{ + nodes: make(map[string]*PermissionNode), + names: make(map[string][]string), + mu: sync.RWMutex{}, + } + // 建立 dummy root 節點 + dummyRootPerm := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "root", + Status: permission.Open, // dummy root 狀態不影響結果 + } + tree.root = &PermissionNode{ + Data: dummyRootPerm, + Children: make([]*PermissionNode, 0), + } + + // 建立測試節點 + // Node A:Open, leaf + permA := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 無父節點 → 掛在 dummy root 下 + Name: "A", + Status: permission.Open, + } + tree.put(permA) + + // Node B:Open, non-leaf + permB := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 掛在 dummy root 下 + Name: "B", + Status: permission.Open, + } + tree.put(permB) + + // Node B1:Open, leaf, Parent = B + permB1 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permB.ID.Hex(), + Name: "B1", + Status: permission.Open, + } + tree.put(permB1) + + // Node B2:Closed, leaf, Parent = B + permB2 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permB.ID.Hex(), + Name: "B2", + Status: permission.Close, + } + tree.put(permB2) + + // Node C:Open, non-leaf,但其子節點皆 Closed → C 不會展開 + permC := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", // 掛在 dummy root 下 + Name: "C", + Status: permission.Close, + } + tree.put(permC) + + // Node C1:Closed, leaf, Parent = C + permC1 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permC.ID.Hex(), + Name: "C1", + Status: permission.Open, + } + tree.put(permC1) + + // Node C2:Closed, leaf, Parent = C + permC2 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permC.ID.Hex(), + Name: "C2", + Status: permission.Open, + } + tree.put(permC2) + + // Node D:Closed, leaf, 無父節點 + permD := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "D", + Status: permission.Close, + } + tree.put(permD) + + // Node E:Closed, leaf, 無父節點 + permE := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: "", + Name: "E", + Status: permission.Open, + } + tree.put(permE) + + // Node E1:Closed, leaf, Parent = E + permE1 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permE.ID.Hex(), + Name: "E1", + Status: permission.Close, + } + tree.put(permE1) + + // Node E2:Closed, leaf, Parent = E + permE2 := entity.Permission{ + ID: primitive.NewObjectID(), + Parent: permE.ID.Hex(), + Name: "E2", + Status: permission.Close, + } + tree.put(permE2) + + // 執行 filterOpenNodes + openNodes, err := tree.filterOpenNodes() + if err != nil { + t.Fatalf("filterOpenNodes returned error: %v", err) + } + + // 預期結果: + // - Node A 應該包含(open 且為葉節點) + // - Node B 應該包含(open 且其子節點 B1 為 open) + // - Node B1 應該包含(open, leaf) + // - Node B2 不包含(closed) + // - Node C 不包含(closed) + // - Node C1, C2 不包含(C Node Close) + // - Node D 不包含(closed) + // - Node E 包含(open) + // - Node E1, E2 不包含(本身Close) + expectedIDs := map[string]bool{ + permA.ID.Hex(): true, + permB.ID.Hex(): true, + permB1.ID.Hex(): true, + permE.ID.Hex(): true, + } + + // 檢查結果是否只包含預期的節點 + for id, perm := range openNodes { + if !expectedIDs[id] { + t.Errorf("Unexpected node in openNodes: id=%s, name=%s", id, perm.Name) + } + delete(expectedIDs, id) + } + + if len(expectedIDs) != 0 { + t.Errorf("Expected nodes not found in openNodes: %v", expectedIDs) + } +}