From e6d798d7bd302a4813ef433686450080e92c8247 Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 1 Apr 2025 18:45:16 +0300 Subject: [PATCH 01/20] chore(proto): Add proto submodule --- .gitmodules | 3 +++ proto | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 proto diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ee51149 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "proto"] + path = proto + url = git@github.com:tab/loki-proto.git diff --git a/proto b/proto new file mode 160000 index 0000000..7c51c5b --- /dev/null +++ b/proto @@ -0,0 +1 @@ +Subproject commit 7c51c5bea903b001276d12d14886a5a3ee7f107e From a3bf2689af600d9500804ea8aaecd38a5bf183f3 Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 1 Apr 2025 18:47:48 +0300 Subject: [PATCH 02/20] feat(grpc) Add gRPC authentication interceptor and permission service Added gRPC authentication interceptor Added permission service with CRUD operations for managing user permissions --- .gitignore | 3 + buf.gen.yaml | 11 + buf.lock | 9 + buf.yaml | 19 + codecov.yaml | 3 + go.mod | 13 +- go.sum | 29 + internal/app/app.go | 45 +- internal/app/errors/errors.go | 3 + .../app/rpcs/interceptors/authentication.go | 66 ++ .../rpcs/interceptors/authentication_mock.go | 56 ++ .../rpcs/interceptors/authentication_test.go | 195 ++++++ internal/app/rpcs/interceptors/module.go | 7 + internal/app/rpcs/module.go | 15 + .../app/rpcs/proto/sso/v1/pagination.pb.go | 197 ++++++ .../app/rpcs/proto/sso/v1/permission.pb.go | 592 ++++++++++++++++++ .../rpcs/proto/sso/v1/permission_grpc.pb.go | 278 ++++++++ internal/app/rpcs/registry.go | 23 + internal/app/rpcs/registry_test.go | 25 + internal/app/rpcs/services/module.go | 7 + internal/app/rpcs/services/permission_mock.go | 291 +++++++++ internal/app/rpcs/services/permissions.go | 179 ++++++ .../app/rpcs/services/permissions_test.go | 474 ++++++++++++++ internal/config/config.go | 4 + internal/config/config_test.go | 2 + internal/config/server/grpc.go | 142 +++++ internal/config/server/grpc_mock.go | 69 ++ internal/config/server/grpc_test.go | 149 +++++ internal/config/server/module.go | 5 +- internal/config/server/server.go | 12 +- internal/config/server/server_mock.go | 34 +- internal/config/server/server_test.go | 10 +- pkg/spec/tls.go | 139 ++++ pkg/spec/tls_test.go | 109 ++++ proto | 2 +- 35 files changed, 3177 insertions(+), 40 deletions(-) create mode 100644 buf.gen.yaml create mode 100644 buf.lock create mode 100644 buf.yaml create mode 100644 internal/app/rpcs/interceptors/authentication.go create mode 100644 internal/app/rpcs/interceptors/authentication_mock.go create mode 100644 internal/app/rpcs/interceptors/authentication_test.go create mode 100644 internal/app/rpcs/interceptors/module.go create mode 100644 internal/app/rpcs/module.go create mode 100644 internal/app/rpcs/proto/sso/v1/pagination.pb.go create mode 100644 internal/app/rpcs/proto/sso/v1/permission.pb.go create mode 100644 internal/app/rpcs/proto/sso/v1/permission_grpc.pb.go create mode 100644 internal/app/rpcs/registry.go create mode 100644 internal/app/rpcs/registry_test.go create mode 100644 internal/app/rpcs/services/module.go create mode 100644 internal/app/rpcs/services/permission_mock.go create mode 100644 internal/app/rpcs/services/permissions.go create mode 100644 internal/app/rpcs/services/permissions_test.go create mode 100644 internal/config/server/grpc.go create mode 100644 internal/config/server/grpc_mock.go create mode 100644 internal/config/server/grpc_test.go create mode 100644 pkg/spec/tls.go create mode 100644 pkg/spec/tls_test.go diff --git a/.gitignore b/.gitignore index 2ae6076..cb65a64 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ cmd/loki/main cmd/loki/loki +# Certificates +certs/ + # Dependency directories (remove the comment below to include it) vendor/ diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..2162326 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,11 @@ +version: v2 +clean: true +plugins: + - remote: buf.build/protocolbuffers/go + out: internal/app/rpcs/proto + opt: paths=source_relative + - remote: buf.build/grpc/go + out: internal/app/rpcs/proto + opt: paths=source_relative +inputs: + - directory: proto diff --git a/buf.lock b/buf.lock new file mode 100644 index 0000000..0931da0 --- /dev/null +++ b/buf.lock @@ -0,0 +1,9 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 0409229c37804d6187ee0806eb4eebce + digest: b5:795db9d3a6e066dc61d99ac651fa7f136171869abe2211ca272dd84aada7bc4583b9508249fa5b61300a5b1fe8b6dbf6edbc088aa0345d1ccb9fff705e3d48e9 + - name: buf.build/googleapis/googleapis + commit: 751cbe31638d43a9bfb6162cd2352e67 + digest: b5:51ba5c31f244fd74420f0e66d13f2b5dd6024dcfe1a29dc45bd8f6e61c1444c828b9add9e7dd25a4513ebbee8097a970e0712a2e2cd955c2d60cf8905204f51a diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..06213d3 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,19 @@ +version: v2 +modules: + - path: proto + name: buf.build/tab/loki +deps: + - buf.build/bufbuild/protovalidate + - buf.build/googleapis/googleapis +lint: + use: + - STANDARD + except: + - PACKAGE_VERSION_SUFFIX + - SERVICE_SUFFIX + - PACKAGE_DIRECTORY_MATCH + - RPC_REQUEST_STANDARD_NAME + - RPC_RESPONSE_STANDARD_NAME +breaking: + use: + - FILE diff --git a/codecov.yaml b/codecov.yaml index 7e2ffaa..9108411 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -23,4 +23,7 @@ ignore: - "internal/app/repositories/db/*.go" - "**/*_mock.go" - "**/*_test.go" + - "**/*.pb.go" + - "**/*.pb.gw.go" + - "**/*.sql.go" - "**/module.go" diff --git a/go.mod b/go.mod index 4d60fb3..d60060e 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module loki go 1.24 require ( + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 + github.com/bufbuild/protovalidate-go v0.9.1 github.com/exaring/otelpgx v0.9.0 github.com/go-chi/chi/v5 v5.2.1 github.com/go-chi/cors v1.2.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 github.com/jackc/pgx/v5 v5.7.4 github.com/joho/godotenv v1.5.1 github.com/redis/go-redis/extra/redisotel/v9 v9.7.3 @@ -16,6 +19,7 @@ require ( github.com/stretchr/testify v1.10.0 github.com/tab/mobileid v0.1.1 github.com/tab/smartid v0.2.1 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 @@ -23,9 +27,13 @@ require ( go.opentelemetry.io/otel/trace v1.35.0 go.uber.org/fx v1.23.0 go.uber.org/mock v0.5.0 + google.golang.org/grpc v1.71.0 + google.golang.org/protobuf v1.36.5 ) require ( + cel.dev/expr v0.19.1 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -33,6 +41,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/google/cel-go v0.23.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -42,6 +51,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.7.3 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect @@ -49,13 +59,12 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.uber.org/zap v1.26.0 // indirect golang.org/x/crypto v0.33.0 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/net v0.35.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/sys v0.30.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.71.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 985d051..74c07b1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,17 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1 h1:j+l4+E1EEo83GVIxuqinfFOTyImSQUH90WfufE86xaI= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.5-20250307204501-0409229c3780.1/go.mod h1:eOqrCVUfhh7SLo00urDe/XhJHljj0dWMZirS0aX7cmc= +cel.dev/expr v0.19.1 h1:NciYrtDRIR0lNCnH1LFJegdjspNx9fI59O7TWcua/W4= +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bufbuild/protovalidate-go v0.9.1 h1:cdrIA33994yCcJyEIZRL36ZGTe9UDM/WHs5MBHEimiE= +github.com/bufbuild/protovalidate-go v0.9.1/go.mod h1:5jptBxfvlY51RhX32zR6875JfPBRXUsQjyZjm/NqkLQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -12,6 +22,8 @@ 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/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/exaring/otelpgx v0.9.0 h1:Bo0RIhBNrzLlVzih46qBy/KQRvRs9vwRbgT/fE363NM= github.com/exaring/otelpgx v0.9.0/go.mod h1:ANkRZDfgfmN6yJS1xKMkshbnsHO8at5sYwtVEYOX8hc= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= @@ -30,10 +42,14 @@ github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeD github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.23.0 h1:knsnzeUOcREUFo0ZFJqZI8Rk6uEVyobAlir7GEbf5v0= +github.com/google/cel-go v0.23.0/go.mod h1:52Pb6QsDbC5kvgxvZhiL9QX1oZEkcUF/ZqaPx1J5Wwo= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 h1:KcFzXwzM/kGhIRHvc8jdixfIJjVzuUJdnv+5xsPutog= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -70,9 +86,16 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.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= 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/tab/mobileid v0.1.1 h1:W9BGW2uINdJTvr7U1wD6j+HRs8CnLremRYxZ1vcvLt4= @@ -81,6 +104,8 @@ github.com/tab/smartid v0.2.1 h1:yULUHswCP9b9UaXEJhSYLupYRqR7+heF9cSgobqLB1s= github.com/tab/smartid v0.2.1/go.mod h1:Z41bRzEFaQAf+xdCXxz++TP6zLzjcQ+wC51tnLY3KnU= 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/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= @@ -111,8 +136,12 @@ go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/app/app.go b/internal/app/app.go index a06fcf6..6d8cc47 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -5,13 +5,14 @@ import ( "net/http" "time" - "github.com/tab/smartid" "github.com/tab/mobileid" + "github.com/tab/smartid" "go.uber.org/fx" "loki/internal/app/controllers" "loki/internal/app/controllers/backoffice" "loki/internal/app/repositories" + "loki/internal/app/rpcs" "loki/internal/app/services" "loki/internal/app/services/authentication" "loki/internal/app/workers" @@ -35,41 +36,69 @@ var Module = fx.Options( services.Module, workers.Module, + rpcs.Module, + middlewares.Module, server.Module, router.Module, telemetry.Module, - fx.Invoke(registerHooks), + fx.Invoke(registerWebServer), + fx.Invoke(registerGrpcServer), fx.Invoke(registerWorkers), fx.Invoke(registerTelemetry), ) -func registerHooks( +func registerWebServer( lifecycle fx.Lifecycle, cfg *config.Config, - server server.Server, + server server.WebServer, log *logger.Logger, ) { lifecycle.Append(fx.Hook{ OnStart: func(ctx context.Context) error { - log.Info().Msgf("Starting server in %s environment at %s", cfg.AppEnv, cfg.AppAddr) - + log.Info().Msgf("Starting web server in %s environment at %s", cfg.AppEnv, cfg.AppAddr) go func() { if err := server.Run(); err != nil && err != http.ErrServerClosed { - log.Error().Err(err).Msg("Server failed") + log.Error().Err(err).Msg("Web server start failed") } }() return nil }, OnStop: func(ctx context.Context) error { - log.Info().Msg("Shutting down server...") + shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + log.Info().Msg("Shutting down web server...") + return server.Shutdown(shutdownCtx) + }, + }) +} + +func registerGrpcServer( + lifecycle fx.Lifecycle, + cfg *config.Config, + server server.GrpcServer, + log *logger.Logger, +) { + lifecycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + log.Info().Msgf("Starting gRPC server in %s environment at %s", cfg.AppEnv, cfg.GrpcAddr) + go func() { + if err := server.Run(); err != nil { + log.Error().Err(err).Msg("gRPC server start failed") + } + }() + return nil + }, + OnStop: func(ctx context.Context) error { shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() + log.Info().Msg("Shutting down gRPC server...") return server.Shutdown(shutdownCtx) }, }) diff --git a/internal/app/errors/errors.go b/internal/app/errors/errors.go index c206b9d..d7b5115 100644 --- a/internal/app/errors/errors.go +++ b/internal/app/errors/errors.go @@ -36,6 +36,9 @@ var ( // ErrEmptyDescription indicates that the description is empty or invalid ErrEmptyDescription = errors.New("empty description") + // ErrInvalidAttributes indicates that the provided attributes are invalid + ErrInvalidAttributes = errors.New("invalid attributes") + // ErrInvalidIdentityNumber indicates that the provided identity number is invalid ErrInvalidIdentityNumber = errors.New("invalid identity number") diff --git a/internal/app/rpcs/interceptors/authentication.go b/internal/app/rpcs/interceptors/authentication.go new file mode 100644 index 0000000..19130a4 --- /dev/null +++ b/internal/app/rpcs/interceptors/authentication.go @@ -0,0 +1,66 @@ +package interceptors + +import ( + "context" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "loki/internal/app/services" + "loki/internal/config/middlewares" + "loki/pkg/jwt" + "loki/pkg/logger" + "loki/pkg/rbac" +) + +const bearerScheme = "Bearer" + +type AuthenticationInterceptor interface { + Authenticate(ctx context.Context) (context.Context, error) +} + +type authenticationInterceptor struct { + jwt jwt.Jwt + users services.Users + log *logger.Logger +} + +func NewAuthenticationInterceptor(jwt jwt.Jwt, users services.Users, log *logger.Logger) AuthenticationInterceptor { + return &authenticationInterceptor{ + jwt: jwt, + users: users, + log: log, + } +} + +func (i *authenticationInterceptor) Authenticate(ctx context.Context) (context.Context, error) { + token, err := auth.AuthFromMD(ctx, bearerScheme) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %v", err) + } + + claims, err := i.jwt.Decode(token) + if err != nil { + i.log.Error().Err(err).Msg("Failed to decode token") + return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %v", err) + } + + user, err := i.users.FindByIdentityNumber(ctx, claims.ID) + if err != nil { + i.log.Error().Err(err).Msg("Failed to find user by identity number") + return nil, status.Errorf(codes.Unauthenticated, "invalid auth token: %v", err) + } + + if !rbac.HasScope(claims.Scope) { + i.log.Error().Msgf("User %s does not have required scope: %s", claims.ID, rbac.SsoServiceType) + return nil, status.Errorf(codes.PermissionDenied, "missing required scope") + } + + ctx = middlewares.NewContextModifier(ctx). + WithClaim(claims). + WithCurrentUser(user). + Context() + + return ctx, nil +} diff --git a/internal/app/rpcs/interceptors/authentication_mock.go b/internal/app/rpcs/interceptors/authentication_mock.go new file mode 100644 index 0000000..2b836a9 --- /dev/null +++ b/internal/app/rpcs/interceptors/authentication_mock.go @@ -0,0 +1,56 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/rpcs/interceptors/authentication.go +// +// Generated by this command: +// +// mockgen -source=internal/app/rpcs/interceptors/authentication.go -destination=internal/app/rpcs/interceptors/authentication_mock.go -package=interceptors +// + +// Package interceptors is a generated GoMock package. +package interceptors + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockAuthenticationInterceptor is a mock of AuthenticationInterceptor interface. +type MockAuthenticationInterceptor struct { + ctrl *gomock.Controller + recorder *MockAuthenticationInterceptorMockRecorder + isgomock struct{} +} + +// MockAuthenticationInterceptorMockRecorder is the mock recorder for MockAuthenticationInterceptor. +type MockAuthenticationInterceptorMockRecorder struct { + mock *MockAuthenticationInterceptor +} + +// NewMockAuthenticationInterceptor creates a new mock instance. +func NewMockAuthenticationInterceptor(ctrl *gomock.Controller) *MockAuthenticationInterceptor { + mock := &MockAuthenticationInterceptor{ctrl: ctrl} + mock.recorder = &MockAuthenticationInterceptorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuthenticationInterceptor) EXPECT() *MockAuthenticationInterceptorMockRecorder { + return m.recorder +} + +// Authenticate mocks base method. +func (m *MockAuthenticationInterceptor) Authenticate(ctx context.Context) (context.Context, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Authenticate", ctx) + ret0, _ := ret[0].(context.Context) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Authenticate indicates an expected call of Authenticate. +func (mr *MockAuthenticationInterceptorMockRecorder) Authenticate(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockAuthenticationInterceptor)(nil).Authenticate), ctx) +} diff --git a/internal/app/rpcs/interceptors/authentication_test.go b/internal/app/rpcs/interceptors/authentication_test.go new file mode 100644 index 0000000..59b3f84 --- /dev/null +++ b/internal/app/rpcs/interceptors/authentication_test.go @@ -0,0 +1,195 @@ +package interceptors + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "loki/pkg/jwt" + "loki/pkg/logger" + + "loki/internal/app/errors" + "loki/internal/app/models" + "loki/internal/app/services" + "loki/internal/config/middlewares" +) + +func Test_AuthenticationInterceptor_Authenticate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockJWT := jwt.NewMockJwt(ctrl) + mockUsers := services.NewMockUsers(ctrl) + log := logger.NewLogger() + + interceptor := NewAuthenticationInterceptor(mockJWT, mockUsers, log) + + userId := uuid.New() + token := "valid-token" + identityNumber := "PNOEE-1234567890" + + type result struct { + code codes.Code + userId uuid.UUID + error bool + } + + tests := []struct { + name string + ctx func() context.Context + before func() + expected result + }{ + { + name: "Success", + ctx: func() context.Context { + md := metadata.New(map[string]string{ + "authorization": "Bearer " + token, + }) + return metadata.NewIncomingContext(context.Background(), md) + }, + before: func() { + mockJWT.EXPECT().Decode(token).Return(&jwt.Payload{ + ID: identityNumber, + Permissions: []string{"read:users"}, + Roles: []string{"admin"}, + Scope: []string{"sso-service"}, + }, nil) + mockUsers.EXPECT().FindByIdentityNumber(gomock.Any(), identityNumber).Return(&models.User{ + ID: userId, + IdentityNumber: identityNumber, + FirstName: "Test", + LastName: "User", + }, nil) + }, + expected: result{ + code: codes.OK, + userId: userId, + error: false, + }, + }, + { + name: "Missing auth header", + ctx: context.Background, + before: func() {}, + expected: result{ + code: codes.Unauthenticated, + userId: uuid.Nil, + error: true, + }, + }, + { + name: "Invalid auth scheme", + ctx: func() context.Context { + md := metadata.New(map[string]string{ + "authorization": "Basic " + token, + }) + return metadata.NewIncomingContext(context.Background(), md) + }, + before: func() {}, + expected: result{ + code: codes.Unauthenticated, + userId: uuid.Nil, + error: true, + }, + }, + { + name: "JWT decode error", + ctx: func() context.Context { + md := metadata.New(map[string]string{ + "authorization": "Bearer " + token, + }) + return metadata.NewIncomingContext(context.Background(), md) + }, + before: func() { + mockJWT.EXPECT().Decode(token).Return(nil, errors.ErrInvalidToken) + }, + expected: result{ + code: codes.Unauthenticated, + userId: uuid.Nil, + error: true, + }, + }, + { + name: "User not found", + ctx: func() context.Context { + md := metadata.New(map[string]string{ + "authorization": "Bearer " + token, + }) + return metadata.NewIncomingContext(context.Background(), md) + }, + before: func() { + mockJWT.EXPECT().Decode(token).Return(&jwt.Payload{ + ID: identityNumber, + Permissions: []string{"read:users"}, + Roles: []string{"admin"}, + Scope: []string{"sso-service"}, + }, nil) + mockUsers.EXPECT().FindByIdentityNumber(gomock.Any(), identityNumber).Return(nil, errors.ErrUserNotFound) + }, + expected: result{ + code: codes.Unauthenticated, + userId: uuid.Nil, + error: true, + }, + }, + { + name: "Missing required scope", + ctx: func() context.Context { + md := metadata.New(map[string]string{ + "authorization": "Bearer " + token, + }) + return metadata.NewIncomingContext(context.Background(), md) + }, + before: func() { + mockJWT.EXPECT().Decode(token).Return(&jwt.Payload{ + ID: identityNumber, + Permissions: []string{"read:users"}, + Roles: []string{"admin"}, + Scope: []string{"not-sso-service-scope"}, + }, nil) + mockUsers.EXPECT().FindByIdentityNumber(gomock.Any(), identityNumber).Return(&models.User{ + ID: userId, + IdentityNumber: identityNumber, + FirstName: "Test", + LastName: "User", + }, nil) + }, + expected: result{ + code: codes.PermissionDenied, + userId: uuid.Nil, + error: true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + resultCtx, err := interceptor.Authenticate(tt.ctx()) + + if tt.expected.error { + assert.Error(t, err) + st, ok := status.FromError(err) + assert.True(t, ok) + assert.Equal(t, tt.expected.code, st.Code()) + } else { + assert.NoError(t, err) + + user, ok := middlewares.CurrentUserFromContext(resultCtx) + assert.True(t, ok) + assert.Equal(t, tt.expected.userId, user.ID) + + claim, ok := middlewares.CurrentClaimFromContext(resultCtx) + assert.True(t, ok) + assert.Equal(t, identityNumber, claim.ID) + } + }) + } +} diff --git a/internal/app/rpcs/interceptors/module.go b/internal/app/rpcs/interceptors/module.go new file mode 100644 index 0000000..f464603 --- /dev/null +++ b/internal/app/rpcs/interceptors/module.go @@ -0,0 +1,7 @@ +package interceptors + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide(NewAuthenticationInterceptor), +) diff --git a/internal/app/rpcs/module.go b/internal/app/rpcs/module.go new file mode 100644 index 0000000..efc21b2 --- /dev/null +++ b/internal/app/rpcs/module.go @@ -0,0 +1,15 @@ +package rpcs + +import ( + "go.uber.org/fx" + + "loki/internal/app/rpcs/interceptors" + "loki/internal/app/rpcs/services" +) + +var Module = fx.Options( + interceptors.Module, + services.Module, + + fx.Provide(NewRegistry), +) diff --git a/internal/app/rpcs/proto/sso/v1/pagination.pb.go b/internal/app/rpcs/proto/sso/v1/pagination.pb.go new file mode 100644 index 0000000..bca0bd8 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/pagination.pb.go @@ -0,0 +1,197 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: sso/v1/pagination.proto + +package ssov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PaginatedListRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit uint64 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset uint64 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PaginatedListRequest) Reset() { + *x = PaginatedListRequest{} + mi := &file_sso_v1_pagination_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PaginatedListRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaginatedListRequest) ProtoMessage() {} + +func (x *PaginatedListRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_pagination_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaginatedListRequest.ProtoReflect.Descriptor instead. +func (*PaginatedListRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_pagination_proto_rawDescGZIP(), []int{0} +} + +func (x *PaginatedListRequest) GetLimit() uint64 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *PaginatedListRequest) GetOffset() uint64 { + if x != nil { + return x.Offset + } + return 0 +} + +type PaginationMeta struct { + state protoimpl.MessageState `protogen:"open.v1"` + Page uint64 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + Per uint64 `protobuf:"varint,2,opt,name=per,proto3" json:"per,omitempty"` + Total uint64 `protobuf:"varint,3,opt,name=total,proto3" json:"total,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PaginationMeta) Reset() { + *x = PaginationMeta{} + mi := &file_sso_v1_pagination_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PaginationMeta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaginationMeta) ProtoMessage() {} + +func (x *PaginationMeta) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_pagination_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaginationMeta.ProtoReflect.Descriptor instead. +func (*PaginationMeta) Descriptor() ([]byte, []int) { + return file_sso_v1_pagination_proto_rawDescGZIP(), []int{1} +} + +func (x *PaginationMeta) GetPage() uint64 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *PaginationMeta) GetPer() uint64 { + if x != nil { + return x.Per + } + return 0 +} + +func (x *PaginationMeta) GetTotal() uint64 { + if x != nil { + return x.Total + } + return 0 +} + +var File_sso_v1_pagination_proto protoreflect.FileDescriptor + +const file_sso_v1_pagination_proto_rawDesc = "" + + "\n" + + "\x17sso/v1/pagination.proto\x12\x06sso.v1\x1a\x1bbuf/validate/validate.proto\"V\n" + + "\x14PaginatedListRequest\x12\x1d\n" + + "\x05limit\x18\x01 \x01(\x04B\a\xbaH\x042\x02(\x01R\x05limit\x12\x1f\n" + + "\x06offset\x18\x02 \x01(\x04B\a\xbaH\x042\x02(\x01R\x06offset\"L\n" + + "\x0ePaginationMeta\x12\x12\n" + + "\x04page\x18\x01 \x01(\x04R\x04page\x12\x10\n" + + "\x03per\x18\x02 \x01(\x04R\x03per\x12\x14\n" + + "\x05total\x18\x03 \x01(\x04R\x05totalB+Z)loki/internal/app/rpcs/proto/sso/v1;ssov1b\x06proto3" + +var ( + file_sso_v1_pagination_proto_rawDescOnce sync.Once + file_sso_v1_pagination_proto_rawDescData []byte +) + +func file_sso_v1_pagination_proto_rawDescGZIP() []byte { + file_sso_v1_pagination_proto_rawDescOnce.Do(func() { + file_sso_v1_pagination_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sso_v1_pagination_proto_rawDesc), len(file_sso_v1_pagination_proto_rawDesc))) + }) + return file_sso_v1_pagination_proto_rawDescData +} + +var file_sso_v1_pagination_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_sso_v1_pagination_proto_goTypes = []any{ + (*PaginatedListRequest)(nil), // 0: sso.v1.PaginatedListRequest + (*PaginationMeta)(nil), // 1: sso.v1.PaginationMeta +} +var file_sso_v1_pagination_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_sso_v1_pagination_proto_init() } +func file_sso_v1_pagination_proto_init() { + if File_sso_v1_pagination_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sso_v1_pagination_proto_rawDesc), len(file_sso_v1_pagination_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_sso_v1_pagination_proto_goTypes, + DependencyIndexes: file_sso_v1_pagination_proto_depIdxs, + MessageInfos: file_sso_v1_pagination_proto_msgTypes, + }.Build() + File_sso_v1_pagination_proto = out.File + file_sso_v1_pagination_proto_goTypes = nil + file_sso_v1_pagination_proto_depIdxs = nil +} diff --git a/internal/app/rpcs/proto/sso/v1/permission.pb.go b/internal/app/rpcs/proto/sso/v1/permission.pb.go new file mode 100644 index 0000000..a3acd2f --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/permission.pb.go @@ -0,0 +1,592 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: sso/v1/permission.proto + +package ssov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Permission represents a user permission object +type Permission struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Permission) Reset() { + *x = Permission{} + mi := &file_sso_v1_permission_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Permission) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Permission) ProtoMessage() {} + +func (x *Permission) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Permission.ProtoReflect.Descriptor instead. +func (*Permission) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{0} +} + +func (x *Permission) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Permission) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Permission) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// ListPermissionsResponse is the response for the List method +type ListPermissionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []*Permission `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` + Meta *PaginationMeta `protobuf:"bytes,2,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListPermissionsResponse) Reset() { + *x = ListPermissionsResponse{} + mi := &file_sso_v1_permission_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListPermissionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListPermissionsResponse) ProtoMessage() {} + +func (x *ListPermissionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListPermissionsResponse.ProtoReflect.Descriptor instead. +func (*ListPermissionsResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{1} +} + +func (x *ListPermissionsResponse) GetData() []*Permission { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListPermissionsResponse) GetMeta() *PaginationMeta { + if x != nil { + return x.Meta + } + return nil +} + +// GetPermissionRequest is the request for the Get method +type GetPermissionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPermissionRequest) Reset() { + *x = GetPermissionRequest{} + mi := &file_sso_v1_permission_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPermissionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPermissionRequest) ProtoMessage() {} + +func (x *GetPermissionRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPermissionRequest.ProtoReflect.Descriptor instead. +func (*GetPermissionRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{2} +} + +func (x *GetPermissionRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetPermissionResponse is the response for the Get method +type GetPermissionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Permission `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetPermissionResponse) Reset() { + *x = GetPermissionResponse{} + mi := &file_sso_v1_permission_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetPermissionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetPermissionResponse) ProtoMessage() {} + +func (x *GetPermissionResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetPermissionResponse.ProtoReflect.Descriptor instead. +func (*GetPermissionResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{3} +} + +func (x *GetPermissionResponse) GetData() *Permission { + if x != nil { + return x.Data + } + return nil +} + +// CreatePermissionRequest is the request for the Create method +type CreatePermissionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePermissionRequest) Reset() { + *x = CreatePermissionRequest{} + mi := &file_sso_v1_permission_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePermissionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePermissionRequest) ProtoMessage() {} + +func (x *CreatePermissionRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePermissionRequest.ProtoReflect.Descriptor instead. +func (*CreatePermissionRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{4} +} + +func (x *CreatePermissionRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreatePermissionRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// CreatePermissionResponse is the response for the Create method +type CreatePermissionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Permission `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreatePermissionResponse) Reset() { + *x = CreatePermissionResponse{} + mi := &file_sso_v1_permission_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreatePermissionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePermissionResponse) ProtoMessage() {} + +func (x *CreatePermissionResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePermissionResponse.ProtoReflect.Descriptor instead. +func (*CreatePermissionResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{5} +} + +func (x *CreatePermissionResponse) GetData() *Permission { + if x != nil { + return x.Data + } + return nil +} + +// UpdatePermissionRequest is the request for the Update method +type UpdatePermissionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdatePermissionRequest) Reset() { + *x = UpdatePermissionRequest{} + mi := &file_sso_v1_permission_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdatePermissionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatePermissionRequest) ProtoMessage() {} + +func (x *UpdatePermissionRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatePermissionRequest.ProtoReflect.Descriptor instead. +func (*UpdatePermissionRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdatePermissionRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdatePermissionRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdatePermissionRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// UpdatePermissionResponse is the response for the Update method +type UpdatePermissionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Permission `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdatePermissionResponse) Reset() { + *x = UpdatePermissionResponse{} + mi := &file_sso_v1_permission_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdatePermissionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatePermissionResponse) ProtoMessage() {} + +func (x *UpdatePermissionResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatePermissionResponse.ProtoReflect.Descriptor instead. +func (*UpdatePermissionResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdatePermissionResponse) GetData() *Permission { + if x != nil { + return x.Data + } + return nil +} + +// DeletePermissionRequest is the request for the Delete method +type DeletePermissionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletePermissionRequest) Reset() { + *x = DeletePermissionRequest{} + mi := &file_sso_v1_permission_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletePermissionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletePermissionRequest) ProtoMessage() {} + +func (x *DeletePermissionRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_permission_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletePermissionRequest.ProtoReflect.Descriptor instead. +func (*DeletePermissionRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_permission_proto_rawDescGZIP(), []int{8} +} + +func (x *DeletePermissionRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_sso_v1_permission_proto protoreflect.FileDescriptor + +const file_sso_v1_permission_proto_rawDesc = "" + + "\n" + + "\x17sso/v1/permission.proto\x12\x06sso.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x17sso/v1/pagination.proto\"s\n" + + "\n" + + "Permission\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1d\n" + + "\x04name\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\"m\n" + + "\x17ListPermissionsResponse\x12&\n" + + "\x04data\x18\x01 \x03(\v2\x12.sso.v1.PermissionR\x04data\x12*\n" + + "\x04meta\x18\x02 \x01(\v2\x16.sso.v1.PaginationMetaR\x04meta\"0\n" + + "\x14GetPermissionRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"?\n" + + "\x15GetPermissionResponse\x12&\n" + + "\x04data\x18\x01 \x01(\v2\x12.sso.v1.PermissionR\x04data\"f\n" + + "\x17CreatePermissionRequest\x12\x1d\n" + + "\x04name\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\"B\n" + + "\x18CreatePermissionResponse\x12&\n" + + "\x04data\x18\x01 \x01(\v2\x12.sso.v1.PermissionR\x04data\"\x80\x01\n" + + "\x17UpdatePermissionRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1d\n" + + "\x04name\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\"B\n" + + "\x18UpdatePermissionResponse\x12&\n" + + "\x04data\x18\x01 \x01(\v2\x12.sso.v1.PermissionR\x04data\"3\n" + + "\x17DeletePermissionRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id2\x85\x03\n" + + "\x11PermissionService\x12G\n" + + "\x04List\x12\x1c.sso.v1.PaginatedListRequest\x1a\x1f.sso.v1.ListPermissionsResponse\"\x00\x12D\n" + + "\x03Get\x12\x1c.sso.v1.GetPermissionRequest\x1a\x1d.sso.v1.GetPermissionResponse\"\x00\x12M\n" + + "\x06Create\x12\x1f.sso.v1.CreatePermissionRequest\x1a .sso.v1.CreatePermissionResponse\"\x00\x12M\n" + + "\x06Update\x12\x1f.sso.v1.UpdatePermissionRequest\x1a .sso.v1.UpdatePermissionResponse\"\x00\x12C\n" + + "\x06Delete\x12\x1f.sso.v1.DeletePermissionRequest\x1a\x16.google.protobuf.Empty\"\x00B+Z)loki/internal/app/rpcs/proto/sso/v1;ssov1b\x06proto3" + +var ( + file_sso_v1_permission_proto_rawDescOnce sync.Once + file_sso_v1_permission_proto_rawDescData []byte +) + +func file_sso_v1_permission_proto_rawDescGZIP() []byte { + file_sso_v1_permission_proto_rawDescOnce.Do(func() { + file_sso_v1_permission_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sso_v1_permission_proto_rawDesc), len(file_sso_v1_permission_proto_rawDesc))) + }) + return file_sso_v1_permission_proto_rawDescData +} + +var file_sso_v1_permission_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_sso_v1_permission_proto_goTypes = []any{ + (*Permission)(nil), // 0: sso.v1.Permission + (*ListPermissionsResponse)(nil), // 1: sso.v1.ListPermissionsResponse + (*GetPermissionRequest)(nil), // 2: sso.v1.GetPermissionRequest + (*GetPermissionResponse)(nil), // 3: sso.v1.GetPermissionResponse + (*CreatePermissionRequest)(nil), // 4: sso.v1.CreatePermissionRequest + (*CreatePermissionResponse)(nil), // 5: sso.v1.CreatePermissionResponse + (*UpdatePermissionRequest)(nil), // 6: sso.v1.UpdatePermissionRequest + (*UpdatePermissionResponse)(nil), // 7: sso.v1.UpdatePermissionResponse + (*DeletePermissionRequest)(nil), // 8: sso.v1.DeletePermissionRequest + (*PaginationMeta)(nil), // 9: sso.v1.PaginationMeta + (*PaginatedListRequest)(nil), // 10: sso.v1.PaginatedListRequest + (*emptypb.Empty)(nil), // 11: google.protobuf.Empty +} +var file_sso_v1_permission_proto_depIdxs = []int32{ + 0, // 0: sso.v1.ListPermissionsResponse.data:type_name -> sso.v1.Permission + 9, // 1: sso.v1.ListPermissionsResponse.meta:type_name -> sso.v1.PaginationMeta + 0, // 2: sso.v1.GetPermissionResponse.data:type_name -> sso.v1.Permission + 0, // 3: sso.v1.CreatePermissionResponse.data:type_name -> sso.v1.Permission + 0, // 4: sso.v1.UpdatePermissionResponse.data:type_name -> sso.v1.Permission + 10, // 5: sso.v1.PermissionService.List:input_type -> sso.v1.PaginatedListRequest + 2, // 6: sso.v1.PermissionService.Get:input_type -> sso.v1.GetPermissionRequest + 4, // 7: sso.v1.PermissionService.Create:input_type -> sso.v1.CreatePermissionRequest + 6, // 8: sso.v1.PermissionService.Update:input_type -> sso.v1.UpdatePermissionRequest + 8, // 9: sso.v1.PermissionService.Delete:input_type -> sso.v1.DeletePermissionRequest + 1, // 10: sso.v1.PermissionService.List:output_type -> sso.v1.ListPermissionsResponse + 3, // 11: sso.v1.PermissionService.Get:output_type -> sso.v1.GetPermissionResponse + 5, // 12: sso.v1.PermissionService.Create:output_type -> sso.v1.CreatePermissionResponse + 7, // 13: sso.v1.PermissionService.Update:output_type -> sso.v1.UpdatePermissionResponse + 11, // 14: sso.v1.PermissionService.Delete:output_type -> google.protobuf.Empty + 10, // [10:15] is the sub-list for method output_type + 5, // [5:10] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_sso_v1_permission_proto_init() } +func file_sso_v1_permission_proto_init() { + if File_sso_v1_permission_proto != nil { + return + } + file_sso_v1_pagination_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sso_v1_permission_proto_rawDesc), len(file_sso_v1_permission_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sso_v1_permission_proto_goTypes, + DependencyIndexes: file_sso_v1_permission_proto_depIdxs, + MessageInfos: file_sso_v1_permission_proto_msgTypes, + }.Build() + File_sso_v1_permission_proto = out.File + file_sso_v1_permission_proto_goTypes = nil + file_sso_v1_permission_proto_depIdxs = nil +} diff --git a/internal/app/rpcs/proto/sso/v1/permission_grpc.pb.go b/internal/app/rpcs/proto/sso/v1/permission_grpc.pb.go new file mode 100644 index 0000000..2101b15 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/permission_grpc.pb.go @@ -0,0 +1,278 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: sso/v1/permission.proto + +package ssov1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + PermissionService_List_FullMethodName = "/sso.v1.PermissionService/List" + PermissionService_Get_FullMethodName = "/sso.v1.PermissionService/Get" + PermissionService_Create_FullMethodName = "/sso.v1.PermissionService/Create" + PermissionService_Update_FullMethodName = "/sso.v1.PermissionService/Update" + PermissionService_Delete_FullMethodName = "/sso.v1.PermissionService/Delete" +) + +// PermissionServiceClient is the client API for PermissionService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Permission service provides CRUD operations for managing permissions +type PermissionServiceClient interface { + List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListPermissionsResponse, error) + Get(ctx context.Context, in *GetPermissionRequest, opts ...grpc.CallOption) (*GetPermissionResponse, error) + Create(ctx context.Context, in *CreatePermissionRequest, opts ...grpc.CallOption) (*CreatePermissionResponse, error) + Update(ctx context.Context, in *UpdatePermissionRequest, opts ...grpc.CallOption) (*UpdatePermissionResponse, error) + Delete(ctx context.Context, in *DeletePermissionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type permissionServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewPermissionServiceClient(cc grpc.ClientConnInterface) PermissionServiceClient { + return &permissionServiceClient{cc} +} + +func (c *permissionServiceClient) List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListPermissionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListPermissionsResponse) + err := c.cc.Invoke(ctx, PermissionService_List_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *permissionServiceClient) Get(ctx context.Context, in *GetPermissionRequest, opts ...grpc.CallOption) (*GetPermissionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetPermissionResponse) + err := c.cc.Invoke(ctx, PermissionService_Get_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *permissionServiceClient) Create(ctx context.Context, in *CreatePermissionRequest, opts ...grpc.CallOption) (*CreatePermissionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreatePermissionResponse) + err := c.cc.Invoke(ctx, PermissionService_Create_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *permissionServiceClient) Update(ctx context.Context, in *UpdatePermissionRequest, opts ...grpc.CallOption) (*UpdatePermissionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdatePermissionResponse) + err := c.cc.Invoke(ctx, PermissionService_Update_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *permissionServiceClient) Delete(ctx context.Context, in *DeletePermissionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, PermissionService_Delete_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PermissionServiceServer is the server API for PermissionService service. +// All implementations must embed UnimplementedPermissionServiceServer +// for forward compatibility. +// +// Permission service provides CRUD operations for managing permissions +type PermissionServiceServer interface { + List(context.Context, *PaginatedListRequest) (*ListPermissionsResponse, error) + Get(context.Context, *GetPermissionRequest) (*GetPermissionResponse, error) + Create(context.Context, *CreatePermissionRequest) (*CreatePermissionResponse, error) + Update(context.Context, *UpdatePermissionRequest) (*UpdatePermissionResponse, error) + Delete(context.Context, *DeletePermissionRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedPermissionServiceServer() +} + +// UnimplementedPermissionServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedPermissionServiceServer struct{} + +func (UnimplementedPermissionServiceServer) List(context.Context, *PaginatedListRequest) (*ListPermissionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedPermissionServiceServer) Get(context.Context, *GetPermissionRequest) (*GetPermissionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (UnimplementedPermissionServiceServer) Create(context.Context, *CreatePermissionRequest) (*CreatePermissionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") +} +func (UnimplementedPermissionServiceServer) Update(context.Context, *UpdatePermissionRequest) (*UpdatePermissionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (UnimplementedPermissionServiceServer) Delete(context.Context, *DeletePermissionRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedPermissionServiceServer) mustEmbedUnimplementedPermissionServiceServer() {} +func (UnimplementedPermissionServiceServer) testEmbeddedByValue() {} + +// UnsafePermissionServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to PermissionServiceServer will +// result in compilation errors. +type UnsafePermissionServiceServer interface { + mustEmbedUnimplementedPermissionServiceServer() +} + +func RegisterPermissionServiceServer(s grpc.ServiceRegistrar, srv PermissionServiceServer) { + // If the following call pancis, it indicates UnimplementedPermissionServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&PermissionService_ServiceDesc, srv) +} + +func _PermissionService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PaginatedListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PermissionServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PermissionService_List_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PermissionServiceServer).List(ctx, req.(*PaginatedListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PermissionService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPermissionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PermissionServiceServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PermissionService_Get_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PermissionServiceServer).Get(ctx, req.(*GetPermissionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PermissionService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreatePermissionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PermissionServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PermissionService_Create_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PermissionServiceServer).Create(ctx, req.(*CreatePermissionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PermissionService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdatePermissionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PermissionServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PermissionService_Update_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PermissionServiceServer).Update(ctx, req.(*UpdatePermissionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _PermissionService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeletePermissionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PermissionServiceServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: PermissionService_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PermissionServiceServer).Delete(ctx, req.(*DeletePermissionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// PermissionService_ServiceDesc is the grpc.ServiceDesc for PermissionService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var PermissionService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sso.v1.PermissionService", + HandlerType: (*PermissionServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _PermissionService_List_Handler, + }, + { + MethodName: "Get", + Handler: _PermissionService_Get_Handler, + }, + { + MethodName: "Create", + Handler: _PermissionService_Create_Handler, + }, + { + MethodName: "Update", + Handler: _PermissionService_Update_Handler, + }, + { + MethodName: "Delete", + Handler: _PermissionService_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sso/v1/permission.proto", +} diff --git a/internal/app/rpcs/registry.go b/internal/app/rpcs/registry.go new file mode 100644 index 0000000..4b9116f --- /dev/null +++ b/internal/app/rpcs/registry.go @@ -0,0 +1,23 @@ +package rpcs + +import ( + "google.golang.org/grpc" + + proto "loki/internal/app/rpcs/proto/sso/v1" +) + +type Registry struct { + permissions proto.PermissionServiceServer +} + +func NewRegistry( + permissions proto.PermissionServiceServer, +) *Registry { + return &Registry{ + permissions: permissions, + } +} + +func (r *Registry) RegisterAll(server *grpc.Server) { + proto.RegisterPermissionServiceServer(server, r.permissions) +} diff --git a/internal/app/rpcs/registry_test.go b/internal/app/rpcs/registry_test.go new file mode 100644 index 0000000..2670586 --- /dev/null +++ b/internal/app/rpcs/registry_test.go @@ -0,0 +1,25 @@ +package rpcs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + + proto "loki/internal/app/rpcs/proto/sso/v1" +) + +type permissionService struct { + proto.UnimplementedPermissionServiceServer +} + +func Test_Registry_RegisterAll(t *testing.T) { + registry := NewRegistry(&permissionService{}) + assert.NotNil(t, registry) + + server := grpc.NewServer() + registry.RegisterAll(server) + + serviceInfo := server.GetServiceInfo() + assert.Contains(t, serviceInfo, "sso.v1.PermissionService") +} diff --git a/internal/app/rpcs/services/module.go b/internal/app/rpcs/services/module.go new file mode 100644 index 0000000..3c85f50 --- /dev/null +++ b/internal/app/rpcs/services/module.go @@ -0,0 +1,7 @@ +package services + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide(NewPermissions), +) diff --git a/internal/app/rpcs/services/permission_mock.go b/internal/app/rpcs/services/permission_mock.go new file mode 100644 index 0000000..d6b83cf --- /dev/null +++ b/internal/app/rpcs/services/permission_mock.go @@ -0,0 +1,291 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/rpcs/proto/sso/v1/permission_grpc.pb.go +// +// Generated by this command: +// +// mockgen -source=internal/app/rpcs/proto/sso/v1/permission_grpc.pb.go -destination=internal/app/rpcs/services/permission_mock.go -package=services +// + +// Package services is a generated GoMock package. +package services + +import ( + context "context" + ssov1 "loki/internal/app/rpcs/proto/sso/v1" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// MockPermissionServiceClient is a mock of PermissionServiceClient interface. +type MockPermissionServiceClient struct { + ctrl *gomock.Controller + recorder *MockPermissionServiceClientMockRecorder + isgomock struct{} +} + +// MockPermissionServiceClientMockRecorder is the mock recorder for MockPermissionServiceClient. +type MockPermissionServiceClientMockRecorder struct { + mock *MockPermissionServiceClient +} + +// NewMockPermissionServiceClient creates a new mock instance. +func NewMockPermissionServiceClient(ctrl *gomock.Controller) *MockPermissionServiceClient { + mock := &MockPermissionServiceClient{ctrl: ctrl} + mock.recorder = &MockPermissionServiceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPermissionServiceClient) EXPECT() *MockPermissionServiceClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockPermissionServiceClient) Create(ctx context.Context, in *ssov1.CreatePermissionRequest, opts ...grpc.CallOption) (*ssov1.CreatePermissionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(*ssov1.CreatePermissionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockPermissionServiceClientMockRecorder) Create(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPermissionServiceClient)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockPermissionServiceClient) Delete(ctx context.Context, in *ssov1.DeletePermissionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockPermissionServiceClientMockRecorder) Delete(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPermissionServiceClient)(nil).Delete), varargs...) +} + +// Get mocks base method. +func (m *MockPermissionServiceClient) Get(ctx context.Context, in *ssov1.GetPermissionRequest, opts ...grpc.CallOption) (*ssov1.GetPermissionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(*ssov1.GetPermissionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPermissionServiceClientMockRecorder) Get(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPermissionServiceClient)(nil).Get), varargs...) +} + +// List mocks base method. +func (m *MockPermissionServiceClient) List(ctx context.Context, in *ssov1.PaginatedListRequest, opts ...grpc.CallOption) (*ssov1.ListPermissionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(*ssov1.ListPermissionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockPermissionServiceClientMockRecorder) List(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPermissionServiceClient)(nil).List), varargs...) +} + +// Update mocks base method. +func (m *MockPermissionServiceClient) Update(ctx context.Context, in *ssov1.UpdatePermissionRequest, opts ...grpc.CallOption) (*ssov1.UpdatePermissionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(*ssov1.UpdatePermissionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockPermissionServiceClientMockRecorder) Update(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPermissionServiceClient)(nil).Update), varargs...) +} + +// MockPermissionServiceServer is a mock of PermissionServiceServer interface. +type MockPermissionServiceServer struct { + ctrl *gomock.Controller + recorder *MockPermissionServiceServerMockRecorder + isgomock struct{} +} + +// MockPermissionServiceServerMockRecorder is the mock recorder for MockPermissionServiceServer. +type MockPermissionServiceServerMockRecorder struct { + mock *MockPermissionServiceServer +} + +// NewMockPermissionServiceServer creates a new mock instance. +func NewMockPermissionServiceServer(ctrl *gomock.Controller) *MockPermissionServiceServer { + mock := &MockPermissionServiceServer{ctrl: ctrl} + mock.recorder = &MockPermissionServiceServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPermissionServiceServer) EXPECT() *MockPermissionServiceServerMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockPermissionServiceServer) Create(arg0 context.Context, arg1 *ssov1.CreatePermissionRequest) (*ssov1.CreatePermissionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*ssov1.CreatePermissionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockPermissionServiceServerMockRecorder) Create(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPermissionServiceServer)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockPermissionServiceServer) Delete(arg0 context.Context, arg1 *ssov1.DeletePermissionRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockPermissionServiceServerMockRecorder) Delete(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPermissionServiceServer)(nil).Delete), arg0, arg1) +} + +// Get mocks base method. +func (m *MockPermissionServiceServer) Get(arg0 context.Context, arg1 *ssov1.GetPermissionRequest) (*ssov1.GetPermissionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(*ssov1.GetPermissionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPermissionServiceServerMockRecorder) Get(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPermissionServiceServer)(nil).Get), arg0, arg1) +} + +// List mocks base method. +func (m *MockPermissionServiceServer) List(arg0 context.Context, arg1 *ssov1.PaginatedListRequest) (*ssov1.ListPermissionsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1) + ret0, _ := ret[0].(*ssov1.ListPermissionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockPermissionServiceServerMockRecorder) List(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPermissionServiceServer)(nil).List), arg0, arg1) +} + +// Update mocks base method. +func (m *MockPermissionServiceServer) Update(arg0 context.Context, arg1 *ssov1.UpdatePermissionRequest) (*ssov1.UpdatePermissionResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) + ret0, _ := ret[0].(*ssov1.UpdatePermissionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockPermissionServiceServerMockRecorder) Update(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPermissionServiceServer)(nil).Update), arg0, arg1) +} + +// mustEmbedUnimplementedPermissionServiceServer mocks base method. +func (m *MockPermissionServiceServer) mustEmbedUnimplementedPermissionServiceServer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedPermissionServiceServer") +} + +// mustEmbedUnimplementedPermissionServiceServer indicates an expected call of mustEmbedUnimplementedPermissionServiceServer. +func (mr *MockPermissionServiceServerMockRecorder) mustEmbedUnimplementedPermissionServiceServer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedPermissionServiceServer", reflect.TypeOf((*MockPermissionServiceServer)(nil).mustEmbedUnimplementedPermissionServiceServer)) +} + +// MockUnsafePermissionServiceServer is a mock of UnsafePermissionServiceServer interface. +type MockUnsafePermissionServiceServer struct { + ctrl *gomock.Controller + recorder *MockUnsafePermissionServiceServerMockRecorder + isgomock struct{} +} + +// MockUnsafePermissionServiceServerMockRecorder is the mock recorder for MockUnsafePermissionServiceServer. +type MockUnsafePermissionServiceServerMockRecorder struct { + mock *MockUnsafePermissionServiceServer +} + +// NewMockUnsafePermissionServiceServer creates a new mock instance. +func NewMockUnsafePermissionServiceServer(ctrl *gomock.Controller) *MockUnsafePermissionServiceServer { + mock := &MockUnsafePermissionServiceServer{ctrl: ctrl} + mock.recorder = &MockUnsafePermissionServiceServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUnsafePermissionServiceServer) EXPECT() *MockUnsafePermissionServiceServerMockRecorder { + return m.recorder +} + +// mustEmbedUnimplementedPermissionServiceServer mocks base method. +func (m *MockUnsafePermissionServiceServer) mustEmbedUnimplementedPermissionServiceServer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedPermissionServiceServer") +} + +// mustEmbedUnimplementedPermissionServiceServer indicates an expected call of mustEmbedUnimplementedPermissionServiceServer. +func (mr *MockUnsafePermissionServiceServerMockRecorder) mustEmbedUnimplementedPermissionServiceServer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedPermissionServiceServer", reflect.TypeOf((*MockUnsafePermissionServiceServer)(nil).mustEmbedUnimplementedPermissionServiceServer)) +} diff --git a/internal/app/rpcs/services/permissions.go b/internal/app/rpcs/services/permissions.go new file mode 100644 index 0000000..cafca05 --- /dev/null +++ b/internal/app/rpcs/services/permissions.go @@ -0,0 +1,179 @@ +package services + +import ( + "context" + + "github.com/bufbuild/protovalidate-go" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +type permissionsService struct { + proto.UnimplementedPermissionServiceServer + permissions services.Permissions + log *logger.Logger +} + +func NewPermissions(permissions services.Permissions, log *logger.Logger) proto.PermissionServiceServer { + return &permissionsService{ + permissions: permissions, + log: log, + } +} + +func (p *permissionsService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListPermissionsResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + } + + pagination := &services.Pagination{ + Page: req.Limit, + PerPage: req.Offset, + } + + rows, total, err := p.permissions.List(ctx, pagination) + if err != nil { + p.log.Error().Err(err).Msg("Failed to fetch permissions") + return nil, status.Error(codes.Internal, "failed to fetch permissions") + } + + collection := make([]*proto.Permission, 0, len(rows)) + for _, row := range rows { + collection = append(collection, &proto.Permission{ + Id: row.ID.String(), + Name: row.Name, + Description: row.Description, + }) + } + + return &proto.ListPermissionsResponse{ + Data: collection, + Meta: &proto.PaginationMeta{ + Page: pagination.Page, + Per: pagination.PerPage, + Total: total, + }, + }, nil +} + +func (p *permissionsService) Get(ctx context.Context, req *proto.GetPermissionRequest) (*proto.GetPermissionResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse permission ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + permission, err := p.permissions.FindById(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get permission") + if errors.Is(err, errors.ErrPermissionNotFound) { + return nil, status.Error(codes.NotFound, "permission not found") + } + return nil, status.Error(codes.Internal, "failed to get permission") + } + + return &proto.GetPermissionResponse{ + Data: &proto.Permission{ + Id: permission.ID.String(), + Name: permission.Name, + Description: permission.Description, + }, + }, nil +} + +func (p *permissionsService) Create(ctx context.Context, req *proto.CreatePermissionRequest) (*proto.CreatePermissionResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + permission, err := p.permissions.Create(ctx, &models.Permission{ + Name: req.Name, + Description: req.Description, + }) + if err != nil { + p.log.Error().Err(err).Str("name", req.Name).Msg("Failed to create permission") + return nil, status.Error(codes.Internal, err.Error()) + } + + return &proto.CreatePermissionResponse{ + Data: &proto.Permission{ + Id: permission.ID.String(), + Name: permission.Name, + Description: permission.Description, + }, + }, nil +} + +func (p *permissionsService) Update(ctx context.Context, req *proto.UpdatePermissionRequest) (*proto.UpdatePermissionResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Invalid UUID format") + return nil, status.Error(codes.InvalidArgument, "invalid permission id format") + } + + permission, err := p.permissions.Update(ctx, &models.Permission{ + ID: id, + Name: req.Name, + Description: req.Description, + }) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update permission") + + switch { + case errors.Is(err, errors.ErrPermissionNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to update permission") + } + } + + return &proto.UpdatePermissionResponse{ + Data: &proto.Permission{ + Id: permission.ID.String(), + Name: permission.Name, + Description: permission.Description, + }, + }, nil +} + +func (p *permissionsService) Delete(ctx context.Context, req *proto.DeletePermissionRequest) (*emptypb.Empty, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse permission ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + _, err = p.permissions.Delete(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete permission") + + switch { + case errors.Is(err, errors.ErrPermissionNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to delete permission") + } + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/app/rpcs/services/permissions_test.go b/internal/app/rpcs/services/permissions_test.go new file mode 100644 index 0000000..ad9f679 --- /dev/null +++ b/internal/app/rpcs/services/permissions_test.go @@ -0,0 +1,474 @@ +package services + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +func Test_Permissions_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissions := services.NewMockPermissions(ctrl) + log := logger.NewLogger() + service := NewPermissions(permissions, log) + + tests := []struct { + name string + before func() + request *proto.PaginatedListRequest + expected *proto.ListPermissionsResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + permissions.EXPECT().List(ctx, gomock.Any()).Return([]models.Permission{ + { + ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), + Name: "read:self", + Description: "Read own data", + }, + { + ID: uuid.MustParse("10000000-1000-1000-3000-000000000002"), + Name: "write:self", + Description: "Write own data", + }, + }, uint64(2), nil) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: &proto.ListPermissionsResponse{ + Data: []*proto.Permission{ + { + Id: "10000000-1000-1000-3000-000000000001", + Name: "read:self", + Description: "Read own data", + }, + { + Id: "10000000-1000-1000-3000-000000000002", + Name: "write:self", + Description: "Write own data", + }, + }, + Meta: &proto.PaginationMeta{ + Page: 1, + Per: 10, + Total: 2, + }, + }, + error: false, + }, + { + name: "Error", + before: func() { + permissions.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, + code: codes.Internal, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.List(ctx, tt.request) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.expected.Data), len(result.Data)) + assert.Equal(t, tt.expected.Meta.Total, result.Meta.Total) + for i, permission := range tt.expected.Data { + assert.Equal(t, permission.Id, result.Data[i].Id) + assert.Equal(t, permission.Name, result.Data[i].Name) + assert.Equal(t, permission.Description, result.Data[i].Description) + } + } + }) + } +} + +func Test_Permissions_Get(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissions := services.NewMockPermissions(ctrl) + log := logger.NewLogger() + service := NewPermissions(permissions, log) + + id := uuid.MustParse("10000000-1000-1000-3000-000000000001") + + tests := []struct { + name string + before func() + req *proto.GetPermissionRequest + expected *proto.GetPermissionResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + permissions.EXPECT().FindById(ctx, id).Return(&models.Permission{ + ID: id, + Name: "read:self", + Description: "Read own data", + }, nil) + }, + req: &proto.GetPermissionRequest{ + Id: id.String(), + }, + expected: &proto.GetPermissionResponse{ + Data: &proto.Permission{ + Id: id.String(), + Name: "read:self", + Description: "Read own data", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + permissions.EXPECT().FindById(ctx, id).Return(nil, errors.ErrPermissionNotFound) + }, + req: &proto.GetPermissionRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.GetPermissionRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Get(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Permissions_Create(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissions := services.NewMockPermissions(ctrl) + log := logger.NewLogger() + service := NewPermissions(permissions, log) + + id := uuid.MustParse("10000000-1000-1000-3000-000000000001") + + tests := []struct { + name string + before func() + req *proto.CreatePermissionRequest + expected *proto.CreatePermissionResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + permissions.EXPECT().Create(ctx, gomock.Any()).Return(&models.Permission{ + ID: id, + Name: "read:self", + Description: "Read own data", + }, nil) + }, + req: &proto.CreatePermissionRequest{ + Name: "read:self", + Description: "Read own data", + }, + expected: &proto.CreatePermissionResponse{ + Data: &proto.Permission{ + Id: id.String(), + Name: "read:self", + Description: "Read own data", + }, + }, + error: false, + }, + { + name: "Internal Error", + before: func() { + permissions.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.CreatePermissionRequest{ + Name: "read:self", + Description: "Read own data", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Validation Error", + req: &proto.CreatePermissionRequest{ + Name: "", + Description: "Read own data", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Create(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Permissions_Update(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissions := services.NewMockPermissions(ctrl) + log := logger.NewLogger() + service := NewPermissions(permissions, log) + + id := uuid.MustParse("10000000-1000-1000-3000-000000000001") + + tests := []struct { + name string + before func() + req *proto.UpdatePermissionRequest + expected *proto.UpdatePermissionResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + permissions.EXPECT().Update(ctx, gomock.Any()).Return(&models.Permission{ + ID: id, + Name: "read:self", + Description: "Read own data updated", + }, nil) + }, + req: &proto.UpdatePermissionRequest{ + Id: id.String(), + Name: "read:self", + Description: "Read own data updated", + }, + expected: &proto.UpdatePermissionResponse{ + Data: &proto.Permission{ + Id: id.String(), + Name: "read:self", + Description: "Read own data updated", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrPermissionNotFound) + }, + req: &proto.UpdatePermissionRequest{ + Id: id.String(), + Name: "read:self", + Description: "Read own data updated", + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.UpdatePermissionRequest{ + Id: id.String(), + Name: "read:self", + Description: "Read own data updated", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.UpdatePermissionRequest{ + Id: "invalid-uuid", + Name: "read:self", + Description: "Read own data updated", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Update(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Permissions_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissions := services.NewMockPermissions(ctrl) + log := logger.NewLogger() + service := NewPermissions(permissions, log) + + id := uuid.MustParse("10000000-1000-1000-3000-000000000001") + + tests := []struct { + name string + before func() + req *proto.DeletePermissionRequest + expected *emptypb.Empty + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + permissions.EXPECT().Delete(ctx, id).Return(true, nil) + }, + req: &proto.DeletePermissionRequest{ + Id: id.String(), + }, + expected: &emptypb.Empty{}, + error: false, + }, + { + name: "Not Found", + before: func() { + permissions.EXPECT().Delete(ctx, id).Return(false, errors.ErrPermissionNotFound) + }, + req: &proto.DeletePermissionRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + permissions.EXPECT().Delete(ctx, id).Return(false, assert.AnError) + }, + req: &proto.DeletePermissionRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.DeletePermissionRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Delete(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 9837d80..ca026f6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,6 +10,7 @@ import ( const ( AppAddr = "0.0.0.0:8080" + GrpcAddr = "0.0.0.0:50051" ClientURL = "http://localhost:3000" DebugLevel = "debug" ) @@ -39,6 +40,7 @@ type Config struct { AppEnv string AppName string AppAddr string + GrpcAddr string ClientURL string SecretKey string CertPath string @@ -66,6 +68,7 @@ func LoadConfig() *Config { } flagAppAddr := flag.String("b", AppAddr, "server address") + flagGrpcAddr := flag.String("g", GrpcAddr, "gRPC server address") flagClientURL := flag.String("c", ClientURL, "client address") flagSecretKey := flag.String("s", "", "JWT secret key") flagCertPath := flag.String("p", "", "certificate path") @@ -78,6 +81,7 @@ func LoadConfig() *Config { AppEnv: env, AppName: getEnvString("APP_NAME"), AppAddr: getFlagOrEnvString(*flagAppAddr, "APP_ADDRESS", AppAddr), + GrpcAddr: getFlagOrEnvString(*flagGrpcAddr, "GRPC_ADDRESS", GrpcAddr), ClientURL: getFlagOrEnvString(*flagClientURL, "CLIENT_URL", ClientURL), SecretKey: getFlagOrEnvString(*flagSecretKey, "SECRET_KEY", ""), diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9f3e5db..4b06426 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -38,6 +38,7 @@ func Test_LoadConfig(t *testing.T) { expected: &Config{ AppEnv: "test", AppAddr: "0.0.0.0:8080", + GrpcAddr: "0.0.0.0:50051", ClientURL: "http://localhost:3000", SecretKey: "jwt-secret-key", CertPath: "./certs", @@ -72,6 +73,7 @@ func Test_LoadConfig(t *testing.T) { assert.Equal(t, tt.expected.AppEnv, result.AppEnv) assert.Equal(t, tt.expected.AppAddr, result.AppAddr) + assert.Equal(t, tt.expected.GrpcAddr, result.GrpcAddr) assert.Equal(t, tt.expected.ClientURL, result.ClientURL) assert.Equal(t, tt.expected.SecretKey, result.SecretKey) assert.Equal(t, tt.expected.CertPath, result.CertPath) diff --git a/internal/config/server/grpc.go b/internal/config/server/grpc.go new file mode 100644 index 0000000..fd266f7 --- /dev/null +++ b/internal/config/server/grpc.go @@ -0,0 +1,142 @@ +package server + +import ( + "context" + "crypto/tls" + "crypto/x509" + "net" + "os" + "path/filepath" + "time" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/keepalive" + + "loki/internal/app/rpcs" + "loki/internal/app/rpcs/interceptors" + "loki/internal/config" + "loki/pkg/logger" +) + +const ( + CaFile = "ca.pem" + CertFile = "server.pem" + KeyFile = "server.key" + + MaxConnectionIdle = 5 * time.Minute + MaxConnectionAge = 5 * time.Minute + MaxConnectionAgeGrace = 1 * time.Minute + KeepaliveTime = 5 * time.Second + KeepaliveTimeout = 1 * time.Second +) + +type GrpcServer interface { + Run() error + Shutdown(ctx context.Context) error +} + +type grpcServer struct { + cfg *config.Config + server *grpc.Server + registry *rpcs.Registry + log *logger.Logger +} + +func NewGrpcServer( + cfg *config.Config, + authInterceptor interceptors.AuthenticationInterceptor, + registry *rpcs.Registry, + log *logger.Logger, +) GrpcServer { + tlsConfig, err := setupTLS(cfg, log) + if err != nil { + return nil + } + + options := keepalive.ServerParameters{ + MaxConnectionIdle: MaxConnectionIdle, + MaxConnectionAge: MaxConnectionAge, + MaxConnectionAgeGrace: MaxConnectionAgeGrace, + Time: KeepaliveTime, + Timeout: KeepaliveTimeout, + } + + server := grpc.NewServer( + grpc.Creds(credentials.NewTLS(tlsConfig)), + grpc.KeepaliveParams(options), + grpc.UnaryInterceptor( + auth.UnaryServerInterceptor(authInterceptor.Authenticate), + ), + grpc.StreamInterceptor( + auth.StreamServerInterceptor(authInterceptor.Authenticate), + ), + grpc.StatsHandler(otelgrpc.NewServerHandler()), + ) + registry.RegisterAll(server) + + return &grpcServer{ + cfg: cfg, + server: server, + registry: registry, + log: log, + } +} + +func (s *grpcServer) Run() error { + listener, err := net.Listen("tcp", s.cfg.GrpcAddr) + if err != nil { + return err + } + + return s.server.Serve(listener) +} + +func (s *grpcServer) Shutdown(ctx context.Context) error { + if s.server == nil { + return nil + } + + done := make(chan struct{}) + go func() { + s.server.GracefulStop() + close(done) + }() + + select { + case <-ctx.Done(): + s.server.Stop() + return ctx.Err() + case <-done: + return nil + } +} + +func setupTLS(cfg *config.Config, log *logger.Logger) (*tls.Config, error) { + caCert, err := os.ReadFile(filepath.Join(cfg.CertPath, CaFile)) + if err != nil { + log.Error().Err(err).Msg("Failed to load CA certificate") + return nil, err + } + + caPool := x509.NewCertPool() + caPool.AppendCertsFromPEM(caCert) + + cert, err := tls.LoadX509KeyPair( + filepath.Join(cfg.CertPath, CertFile), + filepath.Join(cfg.CertPath, KeyFile), + ) + if err != nil { + log.Error().Err(err).Msg("Failed to load server certificate and private key") + return nil, err + } + + return &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientCAs: caPool, + MinVersion: tls.VersionTLS13, + ClientAuth: tls.RequireAndVerifyClientCert, + }, nil +} diff --git a/internal/config/server/grpc_mock.go b/internal/config/server/grpc_mock.go new file mode 100644 index 0000000..4db58c1 --- /dev/null +++ b/internal/config/server/grpc_mock.go @@ -0,0 +1,69 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/config/server/grpc.go +// +// Generated by this command: +// +// mockgen -source=internal/config/server/grpc.go -destination=internal/config/server/grpc_mock.go -package=server +// + +// Package server is a generated GoMock package. +package server + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockGrpcServer is a mock of GrpcServer interface. +type MockGrpcServer struct { + ctrl *gomock.Controller + recorder *MockGrpcServerMockRecorder + isgomock struct{} +} + +// MockGrpcServerMockRecorder is the mock recorder for MockGrpcServer. +type MockGrpcServerMockRecorder struct { + mock *MockGrpcServer +} + +// NewMockGrpcServer creates a new mock instance. +func NewMockGrpcServer(ctrl *gomock.Controller) *MockGrpcServer { + mock := &MockGrpcServer{ctrl: ctrl} + mock.recorder = &MockGrpcServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGrpcServer) EXPECT() *MockGrpcServerMockRecorder { + return m.recorder +} + +// Run mocks base method. +func (m *MockGrpcServer) Run() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Run") + ret0, _ := ret[0].(error) + return ret0 +} + +// Run indicates an expected call of Run. +func (mr *MockGrpcServerMockRecorder) Run() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockGrpcServer)(nil).Run)) +} + +// Shutdown mocks base method. +func (m *MockGrpcServer) Shutdown(ctx context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Shutdown", ctx) + ret0, _ := ret[0].(error) + return ret0 +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockGrpcServerMockRecorder) Shutdown(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockGrpcServer)(nil).Shutdown), ctx) +} diff --git a/internal/config/server/grpc_test.go b/internal/config/server/grpc_test.go new file mode 100644 index 0000000..297ac27 --- /dev/null +++ b/internal/config/server/grpc_test.go @@ -0,0 +1,149 @@ +package server + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "loki/internal/app/rpcs" + "loki/internal/app/rpcs/interceptors" + "loki/internal/config" + "loki/pkg/logger" +) + +func Test_NewGrpcServer(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + certDir := generateTestCertificates(t) + + cfg := &config.Config{ + AppEnv: "test", + GrpcAddr: "localhost:50051", + CertPath: certDir, + } + authInterceptor := interceptors.NewMockAuthenticationInterceptor(ctrl) + registry := &rpcs.Registry{} + log := logger.NewLogger() + + srv := NewGrpcServer(cfg, authInterceptor, registry, log) + assert.NotNil(t, srv) + + s, ok := srv.(*grpcServer) + assert.True(t, ok) + assert.Equal(t, cfg, s.cfg) + assert.NotNil(t, s.server) +} + +func Test_GrpcServer_RunAndShutdown(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + certDir := generateTestCertificates(t) + + cfg := &config.Config{ + AppEnv: "test", + GrpcAddr: "localhost:50051", + CertPath: certDir, + } + authInterceptor := interceptors.NewMockAuthenticationInterceptor(ctrl) + registry := &rpcs.Registry{} + log := logger.NewLogger() + + srv := NewGrpcServer(cfg, authInterceptor, registry, log) + assert.NotNil(t, srv) + + runErrCh := make(chan error, 1) + go func() { + err := srv.Run() + runErrCh <- err + }() + + time.Sleep(100 * time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + err := srv.Shutdown(ctx) + assert.NoError(t, err) + + err = <-runErrCh + assert.NoError(t, err) +} + +func generateTestCertificates(t *testing.T) string { + tempDir, err := os.MkdirTemp("", "tls-test-*") + require.NoError(t, err) + + t.Cleanup(func() { os.RemoveAll(tempDir) }) + + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + caTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{Organization: []string{"ACME CA"}, CommonName: "ACME CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + } + caCertDER, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + require.NoError(t, err) + caCertPath := filepath.Join(tempDir, "ca.pem") + caCertFile, err := os.Create(caCertPath) + require.NoError(t, err) + defer caCertFile.Close() + err = pem.Encode(caCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertDER, + }) + require.NoError(t, err) + + serverKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + serverTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{Organization: []string{"ACME"}, CommonName: "localhost"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost", "127.0.0.1", "0.0.0.0"}, + } + serverCertDER, err := x509.CreateCertificate(rand.Reader, &serverTemplate, &caTemplate, &serverKey.PublicKey, caKey) + require.NoError(t, err) + serverCertPath := filepath.Join(tempDir, "server.pem") + serverCertFile, err := os.Create(serverCertPath) + require.NoError(t, err) + defer serverCertFile.Close() + err = pem.Encode(serverCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: serverCertDER, + }) + require.NoError(t, err) + + serverKeyPath := filepath.Join(tempDir, "server.key") + serverKeyFile, err := os.Create(serverKeyPath) + require.NoError(t, err) + defer serverKeyFile.Close() + err = pem.Encode(serverKeyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(serverKey), + }) + require.NoError(t, err) + + return tempDir +} diff --git a/internal/config/server/module.go b/internal/config/server/module.go index 3a104f0..ae6d243 100644 --- a/internal/config/server/module.go +++ b/internal/config/server/module.go @@ -3,5 +3,8 @@ package server import "go.uber.org/fx" var Module = fx.Options( - fx.Provide(NewServer), + fx.Provide( + NewWebServer, + NewGrpcServer, + ), ) diff --git a/internal/config/server/server.go b/internal/config/server/server.go index 5b8911e..5c6597d 100644 --- a/internal/config/server/server.go +++ b/internal/config/server/server.go @@ -8,17 +8,17 @@ import ( "loki/internal/config" ) -type Server interface { +type WebServer interface { Run() error Shutdown(ctx context.Context) error } -type server struct { +type webServer struct { httpServer *http.Server } -func NewServer(cfg *config.Config, appRouter http.Handler) Server { - return &server{ +func NewWebServer(cfg *config.Config, appRouter http.Handler) WebServer { + return &webServer{ httpServer: &http.Server{ Addr: cfg.AppAddr, Handler: appRouter, @@ -29,10 +29,10 @@ func NewServer(cfg *config.Config, appRouter http.Handler) Server { } } -func (s *server) Run() error { +func (s *webServer) Run() error { return s.httpServer.ListenAndServe() } -func (s *server) Shutdown(ctx context.Context) error { +func (s *webServer) Shutdown(ctx context.Context) error { return s.httpServer.Shutdown(ctx) } diff --git a/internal/config/server/server_mock.go b/internal/config/server/server_mock.go index 0e2fcc5..b5eb194 100644 --- a/internal/config/server/server_mock.go +++ b/internal/config/server/server_mock.go @@ -16,32 +16,32 @@ import ( gomock "go.uber.org/mock/gomock" ) -// MockServer is a mock of Server interface. -type MockServer struct { +// MockWebServer is a mock of WebServer interface. +type MockWebServer struct { ctrl *gomock.Controller - recorder *MockServerMockRecorder + recorder *MockWebServerMockRecorder isgomock struct{} } -// MockServerMockRecorder is the mock recorder for MockServer. -type MockServerMockRecorder struct { - mock *MockServer +// MockWebServerMockRecorder is the mock recorder for MockWebServer. +type MockWebServerMockRecorder struct { + mock *MockWebServer } -// NewMockServer creates a new mock instance. -func NewMockServer(ctrl *gomock.Controller) *MockServer { - mock := &MockServer{ctrl: ctrl} - mock.recorder = &MockServerMockRecorder{mock} +// NewMockWebServer creates a new mock instance. +func NewMockWebServer(ctrl *gomock.Controller) *MockWebServer { + mock := &MockWebServer{ctrl: ctrl} + mock.recorder = &MockWebServerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockServer) EXPECT() *MockServerMockRecorder { +func (m *MockWebServer) EXPECT() *MockWebServerMockRecorder { return m.recorder } // Run mocks base method. -func (m *MockServer) Run() error { +func (m *MockWebServer) Run() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Run") ret0, _ := ret[0].(error) @@ -49,13 +49,13 @@ func (m *MockServer) Run() error { } // Run indicates an expected call of Run. -func (mr *MockServerMockRecorder) Run() *gomock.Call { +func (mr *MockWebServerMockRecorder) Run() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockServer)(nil).Run)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockWebServer)(nil).Run)) } // Shutdown mocks base method. -func (m *MockServer) Shutdown(ctx context.Context) error { +func (m *MockWebServer) Shutdown(ctx context.Context) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Shutdown", ctx) ret0, _ := ret[0].(error) @@ -63,7 +63,7 @@ func (m *MockServer) Shutdown(ctx context.Context) error { } // Shutdown indicates an expected call of Shutdown. -func (mr *MockServerMockRecorder) Shutdown(ctx any) *gomock.Call { +func (mr *MockWebServerMockRecorder) Shutdown(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockServer)(nil).Shutdown), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockWebServer)(nil).Shutdown), ctx) } diff --git a/internal/config/server/server_test.go b/internal/config/server/server_test.go index 5755c75..b0817bd 100644 --- a/internal/config/server/server_test.go +++ b/internal/config/server/server_test.go @@ -16,7 +16,7 @@ import ( "loki/internal/config/router" ) -func Test_NewServer(t *testing.T) { +func Test_NewWebServer(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -87,10 +87,10 @@ func Test_NewServer(t *testing.T) { mockBackofficeUsersController, ) - srv := NewServer(cfg, appRouter) + srv := NewWebServer(cfg, appRouter) assert.NotNil(t, srv) - s, ok := srv.(*server) + s, ok := srv.(*webServer) assert.True(t, ok) assert.Equal(t, cfg.AppAddr, s.httpServer.Addr) @@ -100,13 +100,13 @@ func Test_NewServer(t *testing.T) { assert.Equal(t, 120*time.Second, s.httpServer.IdleTimeout) } -func Test_Server_RunAndShutdown(t *testing.T) { +func Test_WebServer_RunAndShutdown(t *testing.T) { cfg := &config.Config{ AppEnv: "test", AppAddr: "localhost:5000", } handler := http.NewServeMux() - srv := NewServer(cfg, handler) + srv := NewWebServer(cfg, handler) runErrCh := make(chan error, 1) go func() { diff --git a/pkg/spec/tls.go b/pkg/spec/tls.go new file mode 100644 index 0000000..31f3f3f --- /dev/null +++ b/pkg/spec/tls.go @@ -0,0 +1,139 @@ +package spec + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + CaFile = "ca.pem" + ClientCertFile = "client.pem" + ClientKeyFile = "client.key" + ServerCertFile = "server.pem" + ServerKeyFile = "server.key" +) + +func GenerateCertificates(t *testing.T) string { + tempDir, err := os.MkdirTemp("", "tls-test-*") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tempDir) }) + + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + caTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{Organization: []string{"Test CA"}, CommonName: "Test CA"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + caCertDER, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + require.NoError(t, err) + + caCertPath := filepath.Join(tempDir, CaFile) + caCertFile, err := os.Create(caCertPath) + require.NoError(t, err) + defer caCertFile.Close() + + err = pem.Encode(caCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caCertDER, + }) + require.NoError(t, err) + + serverKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + serverTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{Organization: []string{"Test Server"}, CommonName: "localhost"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, + } + + serverCertDER, err := x509.CreateCertificate(rand.Reader, &serverTemplate, &caTemplate, &serverKey.PublicKey, caKey) + require.NoError(t, err) + + serverCertPath := filepath.Join(tempDir, ServerCertFile) + serverCertFile, err := os.Create(serverCertPath) + require.NoError(t, err) + defer serverCertFile.Close() + + err = pem.Encode(serverCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: serverCertDER, + }) + require.NoError(t, err) + + serverKeyPath := filepath.Join(tempDir, ServerKeyFile) + serverKeyFile, err := os.Create(serverKeyPath) + require.NoError(t, err) + defer serverKeyFile.Close() + + err = pem.Encode(serverKeyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(serverKey), + }) + require.NoError(t, err) + + clientKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + clientTemplate := x509.Certificate{ + SerialNumber: big.NewInt(3), + Subject: pkix.Name{Organization: []string{"Test Client"}, CommonName: "client"}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + + clientCertDER, err := x509.CreateCertificate(rand.Reader, &clientTemplate, &caTemplate, &clientKey.PublicKey, caKey) + require.NoError(t, err) + + clientCertPath := filepath.Join(tempDir, ClientCertFile) + clientCertFile, err := os.Create(clientCertPath) + require.NoError(t, err) + defer clientCertFile.Close() + + err = pem.Encode(clientCertFile, &pem.Block{ + Type: "CERTIFICATE", + Bytes: clientCertDER, + }) + require.NoError(t, err) + + clientKeyPath := filepath.Join(tempDir, ClientKeyFile) + clientKeyFile, err := os.Create(clientKeyPath) + require.NoError(t, err) + defer clientKeyFile.Close() + + err = pem.Encode(clientKeyFile, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(clientKey), + }) + require.NoError(t, err) + + return tempDir +} diff --git a/pkg/spec/tls_test.go b/pkg/spec/tls_test.go new file mode 100644 index 0000000..b09134b --- /dev/null +++ b/pkg/spec/tls_test.go @@ -0,0 +1,109 @@ +package spec + +import ( + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_GenerateCertificates(t *testing.T) { + certDir := GenerateCertificates(t) + + _, err := os.Stat(certDir) + assert.NoError(t, err) + + expectedFiles := []string{ + CaFile, + ServerCertFile, + ServerKeyFile, + ClientCertFile, + ClientKeyFile, + } + + for _, filename := range expectedFiles { + path := filepath.Join(certDir, filename) + _, err = os.Stat(path) + assert.NoError(t, err) + } + + caCertPath := filepath.Join(certDir, CaFile) + caCertData, err := os.ReadFile(caCertPath) + require.NoError(t, err) + + caCertBlock, _ := pem.Decode(caCertData) + require.NotNil(t, caCertBlock) + assert.Equal(t, "CERTIFICATE", caCertBlock.Type) + + caCert, err := x509.ParseCertificate(caCertBlock.Bytes) + require.NoError(t, err) + + assert.True(t, caCert.IsCA) + assert.Equal(t, "Test CA", caCert.Subject.CommonName) + + serverCertPath := filepath.Join(certDir, ServerCertFile) + serverCertData, err := os.ReadFile(serverCertPath) + require.NoError(t, err) + + serverCertBlock, _ := pem.Decode(serverCertData) + require.NotNil(t, serverCertBlock) + assert.Equal(t, "CERTIFICATE", serverCertBlock.Type) + + serverCert, err := x509.ParseCertificate(serverCertBlock.Bytes) + require.NoError(t, err) + + assert.Equal(t, "localhost", serverCert.Subject.CommonName) + assert.Contains(t, serverCert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + assert.Contains(t, serverCert.DNSNames, "localhost") + + clientCertPath := filepath.Join(certDir, ClientCertFile) + clientCertData, err := os.ReadFile(clientCertPath) + require.NoError(t, err) + + clientCertBlock, _ := pem.Decode(clientCertData) + require.NotNil(t, clientCertBlock) + assert.Equal(t, "CERTIFICATE", clientCertBlock.Type) + + clientCert, err := x509.ParseCertificate(clientCertBlock.Bytes) + require.NoError(t, err) + + assert.Equal(t, "client", clientCert.Subject.CommonName) + assert.Contains(t, clientCert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + + roots := x509.NewCertPool() + roots.AddCert(caCert) + + opts := x509.VerifyOptions{ + Roots: roots, + } + + serverOpts := opts + serverOpts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + _, err = serverCert.Verify(serverOpts) + assert.NoError(t, err) + + clientOpts := opts + clientOpts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth} + _, err = clientCert.Verify(clientOpts) + assert.NoError(t, err) + + serverKeyPath := filepath.Join(certDir, ServerKeyFile) + serverKeyData, err := os.ReadFile(serverKeyPath) + require.NoError(t, err) + + serverKeyBlock, _ := pem.Decode(serverKeyData) + require.NotNil(t, serverKeyBlock) + assert.Equal(t, "RSA PRIVATE KEY", serverKeyBlock.Type) + + clientKeyPath := filepath.Join(certDir, ClientKeyFile) + clientKeyData, err := os.ReadFile(clientKeyPath) + require.NoError(t, err) + + clientKeyBlock, _ := pem.Decode(clientKeyData) + require.NotNil(t, clientKeyBlock) + assert.Equal(t, "RSA PRIVATE KEY", clientKeyBlock.Type) +} diff --git a/proto b/proto index 7c51c5b..85a4c34 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 7c51c5bea903b001276d12d14886a5a3ee7f107e +Subproject commit 85a4c3483886d854663ba6c98b247e456e70125f From 71fd9b2c0dc9e3826c5c6f06ac20e22ff36d0bad Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 1 Apr 2025 22:07:28 +0300 Subject: [PATCH 03/20] feat(grpc) Add gRPC scope service Added scope service with CRUD operations for managing application scopes --- .../app/rpcs/proto/sso/v1/pagination.pb.go | 2 + internal/app/rpcs/proto/sso/v1/scope.pb.go | 591 ++++++++++++++++++ .../app/rpcs/proto/sso/v1/scope_grpc.pb.go | 278 ++++++++ internal/app/rpcs/registry.go | 20 +- internal/app/rpcs/registry_test.go | 9 +- internal/app/rpcs/services/module.go | 1 + .../app/rpcs/services/permissions_test.go | 10 +- internal/app/rpcs/services/scope_mock.go | 291 +++++++++ internal/app/rpcs/services/scopes.go | 179 ++++++ internal/app/rpcs/services/scopes_test.go | 474 ++++++++++++++ 10 files changed, 1841 insertions(+), 14 deletions(-) create mode 100644 internal/app/rpcs/proto/sso/v1/scope.pb.go create mode 100644 internal/app/rpcs/proto/sso/v1/scope_grpc.pb.go create mode 100644 internal/app/rpcs/services/scope_mock.go create mode 100644 internal/app/rpcs/services/scopes.go create mode 100644 internal/app/rpcs/services/scopes_test.go diff --git a/internal/app/rpcs/proto/sso/v1/pagination.pb.go b/internal/app/rpcs/proto/sso/v1/pagination.pb.go index bca0bd8..a90a7f0 100644 --- a/internal/app/rpcs/proto/sso/v1/pagination.pb.go +++ b/internal/app/rpcs/proto/sso/v1/pagination.pb.go @@ -22,6 +22,7 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// PaginatedListRequest is the request for the List method type PaginatedListRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Limit uint64 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` @@ -74,6 +75,7 @@ func (x *PaginatedListRequest) GetOffset() uint64 { return 0 } +// PaginationMeta pagination metadata type PaginationMeta struct { state protoimpl.MessageState `protogen:"open.v1"` Page uint64 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` diff --git a/internal/app/rpcs/proto/sso/v1/scope.pb.go b/internal/app/rpcs/proto/sso/v1/scope.pb.go new file mode 100644 index 0000000..05c1e7a --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/scope.pb.go @@ -0,0 +1,591 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: sso/v1/scope.proto + +package ssov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Scope represents a scope object +type Scope struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Scope) Reset() { + *x = Scope{} + mi := &file_sso_v1_scope_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Scope) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Scope) ProtoMessage() {} + +func (x *Scope) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Scope.ProtoReflect.Descriptor instead. +func (*Scope) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{0} +} + +func (x *Scope) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Scope) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Scope) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// ListScopesResponse is the response for the List method +type ListScopesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []*Scope `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` + Meta *PaginationMeta `protobuf:"bytes,2,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListScopesResponse) Reset() { + *x = ListScopesResponse{} + mi := &file_sso_v1_scope_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListScopesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListScopesResponse) ProtoMessage() {} + +func (x *ListScopesResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListScopesResponse.ProtoReflect.Descriptor instead. +func (*ListScopesResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{1} +} + +func (x *ListScopesResponse) GetData() []*Scope { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListScopesResponse) GetMeta() *PaginationMeta { + if x != nil { + return x.Meta + } + return nil +} + +// GetScopeRequest is the request for the Get method +type GetScopeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetScopeRequest) Reset() { + *x = GetScopeRequest{} + mi := &file_sso_v1_scope_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetScopeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetScopeRequest) ProtoMessage() {} + +func (x *GetScopeRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetScopeRequest.ProtoReflect.Descriptor instead. +func (*GetScopeRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{2} +} + +func (x *GetScopeRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetScopeResponse is the response for the Get method +type GetScopeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Scope `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetScopeResponse) Reset() { + *x = GetScopeResponse{} + mi := &file_sso_v1_scope_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetScopeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetScopeResponse) ProtoMessage() {} + +func (x *GetScopeResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetScopeResponse.ProtoReflect.Descriptor instead. +func (*GetScopeResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{3} +} + +func (x *GetScopeResponse) GetData() *Scope { + if x != nil { + return x.Data + } + return nil +} + +// CreateScopeRequest is the request for the Create method +type CreateScopeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateScopeRequest) Reset() { + *x = CreateScopeRequest{} + mi := &file_sso_v1_scope_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateScopeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateScopeRequest) ProtoMessage() {} + +func (x *CreateScopeRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateScopeRequest.ProtoReflect.Descriptor instead. +func (*CreateScopeRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateScopeRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateScopeRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// CreateScopeResponse is the response for the Create method +type CreateScopeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Scope `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateScopeResponse) Reset() { + *x = CreateScopeResponse{} + mi := &file_sso_v1_scope_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateScopeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateScopeResponse) ProtoMessage() {} + +func (x *CreateScopeResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateScopeResponse.ProtoReflect.Descriptor instead. +func (*CreateScopeResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateScopeResponse) GetData() *Scope { + if x != nil { + return x.Data + } + return nil +} + +// UpdateScopeRequest is the request for the Update method +type UpdateScopeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateScopeRequest) Reset() { + *x = UpdateScopeRequest{} + mi := &file_sso_v1_scope_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateScopeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateScopeRequest) ProtoMessage() {} + +func (x *UpdateScopeRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateScopeRequest.ProtoReflect.Descriptor instead. +func (*UpdateScopeRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateScopeRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateScopeRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateScopeRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +// UpdateScopeResponse is the response for the Update method +type UpdateScopeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Scope `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateScopeResponse) Reset() { + *x = UpdateScopeResponse{} + mi := &file_sso_v1_scope_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateScopeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateScopeResponse) ProtoMessage() {} + +func (x *UpdateScopeResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateScopeResponse.ProtoReflect.Descriptor instead. +func (*UpdateScopeResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateScopeResponse) GetData() *Scope { + if x != nil { + return x.Data + } + return nil +} + +// DeleteScopeRequest is the request for the Delete method +type DeleteScopeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteScopeRequest) Reset() { + *x = DeleteScopeRequest{} + mi := &file_sso_v1_scope_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteScopeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteScopeRequest) ProtoMessage() {} + +func (x *DeleteScopeRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_scope_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteScopeRequest.ProtoReflect.Descriptor instead. +func (*DeleteScopeRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_scope_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteScopeRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_sso_v1_scope_proto protoreflect.FileDescriptor + +const file_sso_v1_scope_proto_rawDesc = "" + + "\n" + + "\x12sso/v1/scope.proto\x12\x06sso.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x17sso/v1/pagination.proto\"n\n" + + "\x05Scope\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1d\n" + + "\x04name\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\"c\n" + + "\x12ListScopesResponse\x12!\n" + + "\x04data\x18\x01 \x03(\v2\r.sso.v1.ScopeR\x04data\x12*\n" + + "\x04meta\x18\x02 \x01(\v2\x16.sso.v1.PaginationMetaR\x04meta\"+\n" + + "\x0fGetScopeRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"5\n" + + "\x10GetScopeResponse\x12!\n" + + "\x04data\x18\x01 \x01(\v2\r.sso.v1.ScopeR\x04data\"a\n" + + "\x12CreateScopeRequest\x12\x1d\n" + + "\x04name\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\"8\n" + + "\x13CreateScopeResponse\x12!\n" + + "\x04data\x18\x01 \x01(\v2\r.sso.v1.ScopeR\x04data\"{\n" + + "\x12UpdateScopeRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1d\n" + + "\x04name\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\"8\n" + + "\x13UpdateScopeResponse\x12!\n" + + "\x04data\x18\x01 \x01(\v2\r.sso.v1.ScopeR\x04data\".\n" + + "\x12DeleteScopeRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id2\xd8\x02\n" + + "\fScopeService\x12B\n" + + "\x04List\x12\x1c.sso.v1.PaginatedListRequest\x1a\x1a.sso.v1.ListScopesResponse\"\x00\x12:\n" + + "\x03Get\x12\x17.sso.v1.GetScopeRequest\x1a\x18.sso.v1.GetScopeResponse\"\x00\x12C\n" + + "\x06Create\x12\x1a.sso.v1.CreateScopeRequest\x1a\x1b.sso.v1.CreateScopeResponse\"\x00\x12C\n" + + "\x06Update\x12\x1a.sso.v1.UpdateScopeRequest\x1a\x1b.sso.v1.UpdateScopeResponse\"\x00\x12>\n" + + "\x06Delete\x12\x1a.sso.v1.DeleteScopeRequest\x1a\x16.google.protobuf.Empty\"\x00B+Z)loki/internal/app/rpcs/proto/sso/v1;ssov1b\x06proto3" + +var ( + file_sso_v1_scope_proto_rawDescOnce sync.Once + file_sso_v1_scope_proto_rawDescData []byte +) + +func file_sso_v1_scope_proto_rawDescGZIP() []byte { + file_sso_v1_scope_proto_rawDescOnce.Do(func() { + file_sso_v1_scope_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sso_v1_scope_proto_rawDesc), len(file_sso_v1_scope_proto_rawDesc))) + }) + return file_sso_v1_scope_proto_rawDescData +} + +var file_sso_v1_scope_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_sso_v1_scope_proto_goTypes = []any{ + (*Scope)(nil), // 0: sso.v1.Scope + (*ListScopesResponse)(nil), // 1: sso.v1.ListScopesResponse + (*GetScopeRequest)(nil), // 2: sso.v1.GetScopeRequest + (*GetScopeResponse)(nil), // 3: sso.v1.GetScopeResponse + (*CreateScopeRequest)(nil), // 4: sso.v1.CreateScopeRequest + (*CreateScopeResponse)(nil), // 5: sso.v1.CreateScopeResponse + (*UpdateScopeRequest)(nil), // 6: sso.v1.UpdateScopeRequest + (*UpdateScopeResponse)(nil), // 7: sso.v1.UpdateScopeResponse + (*DeleteScopeRequest)(nil), // 8: sso.v1.DeleteScopeRequest + (*PaginationMeta)(nil), // 9: sso.v1.PaginationMeta + (*PaginatedListRequest)(nil), // 10: sso.v1.PaginatedListRequest + (*emptypb.Empty)(nil), // 11: google.protobuf.Empty +} +var file_sso_v1_scope_proto_depIdxs = []int32{ + 0, // 0: sso.v1.ListScopesResponse.data:type_name -> sso.v1.Scope + 9, // 1: sso.v1.ListScopesResponse.meta:type_name -> sso.v1.PaginationMeta + 0, // 2: sso.v1.GetScopeResponse.data:type_name -> sso.v1.Scope + 0, // 3: sso.v1.CreateScopeResponse.data:type_name -> sso.v1.Scope + 0, // 4: sso.v1.UpdateScopeResponse.data:type_name -> sso.v1.Scope + 10, // 5: sso.v1.ScopeService.List:input_type -> sso.v1.PaginatedListRequest + 2, // 6: sso.v1.ScopeService.Get:input_type -> sso.v1.GetScopeRequest + 4, // 7: sso.v1.ScopeService.Create:input_type -> sso.v1.CreateScopeRequest + 6, // 8: sso.v1.ScopeService.Update:input_type -> sso.v1.UpdateScopeRequest + 8, // 9: sso.v1.ScopeService.Delete:input_type -> sso.v1.DeleteScopeRequest + 1, // 10: sso.v1.ScopeService.List:output_type -> sso.v1.ListScopesResponse + 3, // 11: sso.v1.ScopeService.Get:output_type -> sso.v1.GetScopeResponse + 5, // 12: sso.v1.ScopeService.Create:output_type -> sso.v1.CreateScopeResponse + 7, // 13: sso.v1.ScopeService.Update:output_type -> sso.v1.UpdateScopeResponse + 11, // 14: sso.v1.ScopeService.Delete:output_type -> google.protobuf.Empty + 10, // [10:15] is the sub-list for method output_type + 5, // [5:10] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_sso_v1_scope_proto_init() } +func file_sso_v1_scope_proto_init() { + if File_sso_v1_scope_proto != nil { + return + } + file_sso_v1_pagination_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sso_v1_scope_proto_rawDesc), len(file_sso_v1_scope_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sso_v1_scope_proto_goTypes, + DependencyIndexes: file_sso_v1_scope_proto_depIdxs, + MessageInfos: file_sso_v1_scope_proto_msgTypes, + }.Build() + File_sso_v1_scope_proto = out.File + file_sso_v1_scope_proto_goTypes = nil + file_sso_v1_scope_proto_depIdxs = nil +} diff --git a/internal/app/rpcs/proto/sso/v1/scope_grpc.pb.go b/internal/app/rpcs/proto/sso/v1/scope_grpc.pb.go new file mode 100644 index 0000000..c650ba2 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/scope_grpc.pb.go @@ -0,0 +1,278 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: sso/v1/scope.proto + +package ssov1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ScopeService_List_FullMethodName = "/sso.v1.ScopeService/List" + ScopeService_Get_FullMethodName = "/sso.v1.ScopeService/Get" + ScopeService_Create_FullMethodName = "/sso.v1.ScopeService/Create" + ScopeService_Update_FullMethodName = "/sso.v1.ScopeService/Update" + ScopeService_Delete_FullMethodName = "/sso.v1.ScopeService/Delete" +) + +// ScopeServiceClient is the client API for ScopeService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Scope service provides CRUD operations for managing scopes +type ScopeServiceClient interface { + List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListScopesResponse, error) + Get(ctx context.Context, in *GetScopeRequest, opts ...grpc.CallOption) (*GetScopeResponse, error) + Create(ctx context.Context, in *CreateScopeRequest, opts ...grpc.CallOption) (*CreateScopeResponse, error) + Update(ctx context.Context, in *UpdateScopeRequest, opts ...grpc.CallOption) (*UpdateScopeResponse, error) + Delete(ctx context.Context, in *DeleteScopeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type scopeServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewScopeServiceClient(cc grpc.ClientConnInterface) ScopeServiceClient { + return &scopeServiceClient{cc} +} + +func (c *scopeServiceClient) List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListScopesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListScopesResponse) + err := c.cc.Invoke(ctx, ScopeService_List_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) Get(ctx context.Context, in *GetScopeRequest, opts ...grpc.CallOption) (*GetScopeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetScopeResponse) + err := c.cc.Invoke(ctx, ScopeService_Get_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) Create(ctx context.Context, in *CreateScopeRequest, opts ...grpc.CallOption) (*CreateScopeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateScopeResponse) + err := c.cc.Invoke(ctx, ScopeService_Create_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) Update(ctx context.Context, in *UpdateScopeRequest, opts ...grpc.CallOption) (*UpdateScopeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateScopeResponse) + err := c.cc.Invoke(ctx, ScopeService_Update_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *scopeServiceClient) Delete(ctx context.Context, in *DeleteScopeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ScopeService_Delete_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ScopeServiceServer is the server API for ScopeService service. +// All implementations must embed UnimplementedScopeServiceServer +// for forward compatibility. +// +// Scope service provides CRUD operations for managing scopes +type ScopeServiceServer interface { + List(context.Context, *PaginatedListRequest) (*ListScopesResponse, error) + Get(context.Context, *GetScopeRequest) (*GetScopeResponse, error) + Create(context.Context, *CreateScopeRequest) (*CreateScopeResponse, error) + Update(context.Context, *UpdateScopeRequest) (*UpdateScopeResponse, error) + Delete(context.Context, *DeleteScopeRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedScopeServiceServer() +} + +// UnimplementedScopeServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedScopeServiceServer struct{} + +func (UnimplementedScopeServiceServer) List(context.Context, *PaginatedListRequest) (*ListScopesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedScopeServiceServer) Get(context.Context, *GetScopeRequest) (*GetScopeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (UnimplementedScopeServiceServer) Create(context.Context, *CreateScopeRequest) (*CreateScopeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") +} +func (UnimplementedScopeServiceServer) Update(context.Context, *UpdateScopeRequest) (*UpdateScopeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (UnimplementedScopeServiceServer) Delete(context.Context, *DeleteScopeRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedScopeServiceServer) mustEmbedUnimplementedScopeServiceServer() {} +func (UnimplementedScopeServiceServer) testEmbeddedByValue() {} + +// UnsafeScopeServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ScopeServiceServer will +// result in compilation errors. +type UnsafeScopeServiceServer interface { + mustEmbedUnimplementedScopeServiceServer() +} + +func RegisterScopeServiceServer(s grpc.ServiceRegistrar, srv ScopeServiceServer) { + // If the following call pancis, it indicates UnimplementedScopeServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ScopeService_ServiceDesc, srv) +} + +func _ScopeService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PaginatedListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ScopeService_List_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).List(ctx, req.(*PaginatedListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetScopeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ScopeService_Get_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).Get(ctx, req.(*GetScopeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateScopeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ScopeService_Create_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).Create(ctx, req.(*CreateScopeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateScopeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ScopeService_Update_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).Update(ctx, req.(*UpdateScopeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ScopeService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteScopeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ScopeServiceServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ScopeService_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ScopeServiceServer).Delete(ctx, req.(*DeleteScopeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ScopeService_ServiceDesc is the grpc.ServiceDesc for ScopeService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ScopeService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sso.v1.ScopeService", + HandlerType: (*ScopeServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _ScopeService_List_Handler, + }, + { + MethodName: "Get", + Handler: _ScopeService_Get_Handler, + }, + { + MethodName: "Create", + Handler: _ScopeService_Create_Handler, + }, + { + MethodName: "Update", + Handler: _ScopeService_Update_Handler, + }, + { + MethodName: "Delete", + Handler: _ScopeService_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sso/v1/scope.proto", +} diff --git a/internal/app/rpcs/registry.go b/internal/app/rpcs/registry.go index 4b9116f..e148819 100644 --- a/internal/app/rpcs/registry.go +++ b/internal/app/rpcs/registry.go @@ -1,23 +1,27 @@ package rpcs import ( - "google.golang.org/grpc" + "google.golang.org/grpc" - proto "loki/internal/app/rpcs/proto/sso/v1" + proto "loki/internal/app/rpcs/proto/sso/v1" ) type Registry struct { - permissions proto.PermissionServiceServer + permissions proto.PermissionServiceServer + scopes proto.ScopeServiceServer } func NewRegistry( - permissions proto.PermissionServiceServer, + permissions proto.PermissionServiceServer, + scopes proto.ScopeServiceServer, ) *Registry { - return &Registry{ - permissions: permissions, - } + return &Registry{ + permissions: permissions, + scopes: scopes, + } } func (r *Registry) RegisterAll(server *grpc.Server) { - proto.RegisterPermissionServiceServer(server, r.permissions) + proto.RegisterPermissionServiceServer(server, r.permissions) + proto.RegisterScopeServiceServer(server, r.scopes) } diff --git a/internal/app/rpcs/registry_test.go b/internal/app/rpcs/registry_test.go index 2670586..b54e556 100644 --- a/internal/app/rpcs/registry_test.go +++ b/internal/app/rpcs/registry_test.go @@ -13,8 +13,15 @@ type permissionService struct { proto.UnimplementedPermissionServiceServer } +type scopeService struct { + proto.UnimplementedScopeServiceServer +} + func Test_Registry_RegisterAll(t *testing.T) { - registry := NewRegistry(&permissionService{}) + registry := NewRegistry( + &permissionService{}, + &scopeService{}, + ) assert.NotNil(t, registry) server := grpc.NewServer() diff --git a/internal/app/rpcs/services/module.go b/internal/app/rpcs/services/module.go index 3c85f50..cb6e89d 100644 --- a/internal/app/rpcs/services/module.go +++ b/internal/app/rpcs/services/module.go @@ -4,4 +4,5 @@ import "go.uber.org/fx" var Module = fx.Options( fx.Provide(NewPermissions), + fx.Provide(NewScopes), ) diff --git a/internal/app/rpcs/services/permissions_test.go b/internal/app/rpcs/services/permissions_test.go index ad9f679..77ab52a 100644 --- a/internal/app/rpcs/services/permissions_test.go +++ b/internal/app/rpcs/services/permissions_test.go @@ -41,12 +41,12 @@ func Test_Permissions_List(t *testing.T) { permissions.EXPECT().List(ctx, gomock.Any()).Return([]models.Permission{ { ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", + Name: models.ReadSelfType, Description: "Read own data", }, { ID: uuid.MustParse("10000000-1000-1000-3000-000000000002"), - Name: "write:self", + Name: models.WriteSelfType, Description: "Write own data", }, }, uint64(2), nil) @@ -138,7 +138,7 @@ func Test_Permissions_Get(t *testing.T) { before: func() { permissions.EXPECT().FindById(ctx, id).Return(&models.Permission{ ID: id, - Name: "read:self", + Name: models.ReadSelfType, Description: "Read own data", }, nil) }, @@ -222,7 +222,7 @@ func Test_Permissions_Create(t *testing.T) { before: func() { permissions.EXPECT().Create(ctx, gomock.Any()).Return(&models.Permission{ ID: id, - Name: "read:self", + Name: models.ReadSelfType, Description: "Read own data", }, nil) }, @@ -309,7 +309,7 @@ func Test_Permissions_Update(t *testing.T) { before: func() { permissions.EXPECT().Update(ctx, gomock.Any()).Return(&models.Permission{ ID: id, - Name: "read:self", + Name: models.ReadSelfType, Description: "Read own data updated", }, nil) }, diff --git a/internal/app/rpcs/services/scope_mock.go b/internal/app/rpcs/services/scope_mock.go new file mode 100644 index 0000000..ba5a02e --- /dev/null +++ b/internal/app/rpcs/services/scope_mock.go @@ -0,0 +1,291 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/rpcs/proto/sso/v1/scope_grpc.pb.go +// +// Generated by this command: +// +// mockgen -source=internal/app/rpcs/proto/sso/v1/scope_grpc.pb.go -destination=internal/app/rpcs/services/scope_mock.go -package=services +// + +// Package services is a generated GoMock package. +package services + +import ( + context "context" + ssov1 "loki/internal/app/rpcs/proto/sso/v1" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// MockScopeServiceClient is a mock of ScopeServiceClient interface. +type MockScopeServiceClient struct { + ctrl *gomock.Controller + recorder *MockScopeServiceClientMockRecorder + isgomock struct{} +} + +// MockScopeServiceClientMockRecorder is the mock recorder for MockScopeServiceClient. +type MockScopeServiceClientMockRecorder struct { + mock *MockScopeServiceClient +} + +// NewMockScopeServiceClient creates a new mock instance. +func NewMockScopeServiceClient(ctrl *gomock.Controller) *MockScopeServiceClient { + mock := &MockScopeServiceClient{ctrl: ctrl} + mock.recorder = &MockScopeServiceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockScopeServiceClient) EXPECT() *MockScopeServiceClientMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockScopeServiceClient) Create(ctx context.Context, in *ssov1.CreateScopeRequest, opts ...grpc.CallOption) (*ssov1.CreateScopeResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Create", varargs...) + ret0, _ := ret[0].(*ssov1.CreateScopeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockScopeServiceClientMockRecorder) Create(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockScopeServiceClient)(nil).Create), varargs...) +} + +// Delete mocks base method. +func (m *MockScopeServiceClient) Delete(ctx context.Context, in *ssov1.DeleteScopeRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Delete", varargs...) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockScopeServiceClientMockRecorder) Delete(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockScopeServiceClient)(nil).Delete), varargs...) +} + +// Get mocks base method. +func (m *MockScopeServiceClient) Get(ctx context.Context, in *ssov1.GetScopeRequest, opts ...grpc.CallOption) (*ssov1.GetScopeResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Get", varargs...) + ret0, _ := ret[0].(*ssov1.GetScopeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockScopeServiceClientMockRecorder) Get(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockScopeServiceClient)(nil).Get), varargs...) +} + +// List mocks base method. +func (m *MockScopeServiceClient) List(ctx context.Context, in *ssov1.PaginatedListRequest, opts ...grpc.CallOption) (*ssov1.ListScopesResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "List", varargs...) + ret0, _ := ret[0].(*ssov1.ListScopesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockScopeServiceClientMockRecorder) List(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockScopeServiceClient)(nil).List), varargs...) +} + +// Update mocks base method. +func (m *MockScopeServiceClient) Update(ctx context.Context, in *ssov1.UpdateScopeRequest, opts ...grpc.CallOption) (*ssov1.UpdateScopeResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, in} + for _, a := range opts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Update", varargs...) + ret0, _ := ret[0].(*ssov1.UpdateScopeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockScopeServiceClientMockRecorder) Update(ctx, in any, opts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, in}, opts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockScopeServiceClient)(nil).Update), varargs...) +} + +// MockScopeServiceServer is a mock of ScopeServiceServer interface. +type MockScopeServiceServer struct { + ctrl *gomock.Controller + recorder *MockScopeServiceServerMockRecorder + isgomock struct{} +} + +// MockScopeServiceServerMockRecorder is the mock recorder for MockScopeServiceServer. +type MockScopeServiceServerMockRecorder struct { + mock *MockScopeServiceServer +} + +// NewMockScopeServiceServer creates a new mock instance. +func NewMockScopeServiceServer(ctrl *gomock.Controller) *MockScopeServiceServer { + mock := &MockScopeServiceServer{ctrl: ctrl} + mock.recorder = &MockScopeServiceServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockScopeServiceServer) EXPECT() *MockScopeServiceServerMockRecorder { + return m.recorder +} + +// Create mocks base method. +func (m *MockScopeServiceServer) Create(arg0 context.Context, arg1 *ssov1.CreateScopeRequest) (*ssov1.CreateScopeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) + ret0, _ := ret[0].(*ssov1.CreateScopeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockScopeServiceServerMockRecorder) Create(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockScopeServiceServer)(nil).Create), arg0, arg1) +} + +// Delete mocks base method. +func (m *MockScopeServiceServer) Delete(arg0 context.Context, arg1 *ssov1.DeleteScopeRequest) (*emptypb.Empty, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) + ret0, _ := ret[0].(*emptypb.Empty) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Delete indicates an expected call of Delete. +func (mr *MockScopeServiceServerMockRecorder) Delete(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockScopeServiceServer)(nil).Delete), arg0, arg1) +} + +// Get mocks base method. +func (m *MockScopeServiceServer) Get(arg0 context.Context, arg1 *ssov1.GetScopeRequest) (*ssov1.GetScopeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) + ret0, _ := ret[0].(*ssov1.GetScopeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockScopeServiceServerMockRecorder) Get(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockScopeServiceServer)(nil).Get), arg0, arg1) +} + +// List mocks base method. +func (m *MockScopeServiceServer) List(arg0 context.Context, arg1 *ssov1.PaginatedListRequest) (*ssov1.ListScopesResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", arg0, arg1) + ret0, _ := ret[0].(*ssov1.ListScopesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockScopeServiceServerMockRecorder) List(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockScopeServiceServer)(nil).List), arg0, arg1) +} + +// Update mocks base method. +func (m *MockScopeServiceServer) Update(arg0 context.Context, arg1 *ssov1.UpdateScopeRequest) (*ssov1.UpdateScopeResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) + ret0, _ := ret[0].(*ssov1.UpdateScopeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockScopeServiceServerMockRecorder) Update(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockScopeServiceServer)(nil).Update), arg0, arg1) +} + +// mustEmbedUnimplementedScopeServiceServer mocks base method. +func (m *MockScopeServiceServer) mustEmbedUnimplementedScopeServiceServer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedScopeServiceServer") +} + +// mustEmbedUnimplementedScopeServiceServer indicates an expected call of mustEmbedUnimplementedScopeServiceServer. +func (mr *MockScopeServiceServerMockRecorder) mustEmbedUnimplementedScopeServiceServer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedScopeServiceServer", reflect.TypeOf((*MockScopeServiceServer)(nil).mustEmbedUnimplementedScopeServiceServer)) +} + +// MockUnsafeScopeServiceServer is a mock of UnsafeScopeServiceServer interface. +type MockUnsafeScopeServiceServer struct { + ctrl *gomock.Controller + recorder *MockUnsafeScopeServiceServerMockRecorder + isgomock struct{} +} + +// MockUnsafeScopeServiceServerMockRecorder is the mock recorder for MockUnsafeScopeServiceServer. +type MockUnsafeScopeServiceServerMockRecorder struct { + mock *MockUnsafeScopeServiceServer +} + +// NewMockUnsafeScopeServiceServer creates a new mock instance. +func NewMockUnsafeScopeServiceServer(ctrl *gomock.Controller) *MockUnsafeScopeServiceServer { + mock := &MockUnsafeScopeServiceServer{ctrl: ctrl} + mock.recorder = &MockUnsafeScopeServiceServerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUnsafeScopeServiceServer) EXPECT() *MockUnsafeScopeServiceServerMockRecorder { + return m.recorder +} + +// mustEmbedUnimplementedScopeServiceServer mocks base method. +func (m *MockUnsafeScopeServiceServer) mustEmbedUnimplementedScopeServiceServer() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "mustEmbedUnimplementedScopeServiceServer") +} + +// mustEmbedUnimplementedScopeServiceServer indicates an expected call of mustEmbedUnimplementedScopeServiceServer. +func (mr *MockUnsafeScopeServiceServerMockRecorder) mustEmbedUnimplementedScopeServiceServer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mustEmbedUnimplementedScopeServiceServer", reflect.TypeOf((*MockUnsafeScopeServiceServer)(nil).mustEmbedUnimplementedScopeServiceServer)) +} diff --git a/internal/app/rpcs/services/scopes.go b/internal/app/rpcs/services/scopes.go new file mode 100644 index 0000000..22cd839 --- /dev/null +++ b/internal/app/rpcs/services/scopes.go @@ -0,0 +1,179 @@ +package services + +import ( + "context" + + "github.com/bufbuild/protovalidate-go" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +type scopesService struct { + proto.UnimplementedScopeServiceServer + scopes services.Scopes + log *logger.Logger +} + +func NewScopes(scopes services.Scopes, log *logger.Logger) proto.ScopeServiceServer { + return &scopesService{ + scopes: scopes, + log: log, + } +} + +func (p *scopesService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListScopesResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + } + + pagination := &services.Pagination{ + Page: req.Limit, + PerPage: req.Offset, + } + + rows, total, err := p.scopes.List(ctx, pagination) + if err != nil { + p.log.Error().Err(err).Msg("Failed to fetch scopes") + return nil, status.Error(codes.Internal, "failed to fetch scopes") + } + + collection := make([]*proto.Scope, 0, len(rows)) + for _, row := range rows { + collection = append(collection, &proto.Scope{ + Id: row.ID.String(), + Name: row.Name, + Description: row.Description, + }) + } + + return &proto.ListScopesResponse{ + Data: collection, + Meta: &proto.PaginationMeta{ + Page: pagination.Page, + Per: pagination.PerPage, + Total: total, + }, + }, nil +} + +func (p *scopesService) Get(ctx context.Context, req *proto.GetScopeRequest) (*proto.GetScopeResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse scope ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + scope, err := p.scopes.FindById(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get scope") + if errors.Is(err, errors.ErrScopeNotFound) { + return nil, status.Error(codes.NotFound, "scope not found") + } + return nil, status.Error(codes.Internal, "failed to get scope") + } + + return &proto.GetScopeResponse{ + Data: &proto.Scope{ + Id: scope.ID.String(), + Name: scope.Name, + Description: scope.Description, + }, + }, nil +} + +func (p *scopesService) Create(ctx context.Context, req *proto.CreateScopeRequest) (*proto.CreateScopeResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + scope, err := p.scopes.Create(ctx, &models.Scope{ + Name: req.Name, + Description: req.Description, + }) + if err != nil { + p.log.Error().Err(err).Str("name", req.Name).Msg("Failed to create scope") + return nil, status.Error(codes.Internal, err.Error()) + } + + return &proto.CreateScopeResponse{ + Data: &proto.Scope{ + Id: scope.ID.String(), + Name: scope.Name, + Description: scope.Description, + }, + }, nil +} + +func (p *scopesService) Update(ctx context.Context, req *proto.UpdateScopeRequest) (*proto.UpdateScopeResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Invalid UUID format") + return nil, status.Error(codes.InvalidArgument, "invalid scope id format") + } + + scope, err := p.scopes.Update(ctx, &models.Scope{ + ID: id, + Name: req.Name, + Description: req.Description, + }) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update scope") + + switch { + case errors.Is(err, errors.ErrScopeNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to update scope") + } + } + + return &proto.UpdateScopeResponse{ + Data: &proto.Scope{ + Id: scope.ID.String(), + Name: scope.Name, + Description: scope.Description, + }, + }, nil +} + +func (p *scopesService) Delete(ctx context.Context, req *proto.DeleteScopeRequest) (*emptypb.Empty, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse scope ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + _, err = p.scopes.Delete(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete scope") + + switch { + case errors.Is(err, errors.ErrScopeNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to delete scope") + } + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/app/rpcs/services/scopes_test.go b/internal/app/rpcs/services/scopes_test.go new file mode 100644 index 0000000..66f5c40 --- /dev/null +++ b/internal/app/rpcs/services/scopes_test.go @@ -0,0 +1,474 @@ +package services + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +func Test_Scopes_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + scopes := services.NewMockScopes(ctrl) + log := logger.NewLogger() + service := NewScopes(scopes, log) + + tests := []struct { + name string + before func() + request *proto.PaginatedListRequest + expected *proto.ListScopesResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + scopes.EXPECT().List(ctx, gomock.Any()).Return([]models.Scope{ + { + ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), + Name: models.SsoServiceType, + Description: "SSO-service scope", + }, + { + ID: uuid.MustParse("10000000-1000-1000-2000-000000000002"), + Name: models.SelfServiceType, + Description: "Self-service scope", + }, + }, uint64(2), nil) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: &proto.ListScopesResponse{ + Data: []*proto.Scope{ + { + Id: "10000000-1000-1000-2000-000000000001", + Name: "sso-service", + Description: "SSO-service scope", + }, + { + Id: "10000000-1000-1000-2000-000000000002", + Name: "self-service", + Description: "Self-service scope", + }, + }, + Meta: &proto.PaginationMeta{ + Page: 1, + Per: 10, + Total: 2, + }, + }, + error: false, + }, + { + name: "Error", + before: func() { + scopes.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, + code: codes.Internal, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.List(ctx, tt.request) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.expected.Data), len(result.Data)) + assert.Equal(t, tt.expected.Meta.Total, result.Meta.Total) + for i, scope := range tt.expected.Data { + assert.Equal(t, scope.Id, result.Data[i].Id) + assert.Equal(t, scope.Name, result.Data[i].Name) + assert.Equal(t, scope.Description, result.Data[i].Description) + } + } + }) + } +} + +func Test_Scopes_Get(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + scopes := services.NewMockScopes(ctrl) + log := logger.NewLogger() + service := NewScopes(scopes, log) + + id := uuid.MustParse("10000000-1000-1000-2000-000000000001") + + tests := []struct { + name string + before func() + req *proto.GetScopeRequest + expected *proto.GetScopeResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + scopes.EXPECT().FindById(ctx, id).Return(&models.Scope{ + ID: id, + Name: models.SsoServiceType, + Description: "SSO-service scope", + }, nil) + }, + req: &proto.GetScopeRequest{ + Id: id.String(), + }, + expected: &proto.GetScopeResponse{ + Data: &proto.Scope{ + Id: id.String(), + Name: "sso-service", + Description: "SSO-service scope", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + scopes.EXPECT().FindById(ctx, id).Return(nil, errors.ErrScopeNotFound) + }, + req: &proto.GetScopeRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.GetScopeRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Get(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Scopes_Create(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + scopes := services.NewMockScopes(ctrl) + log := logger.NewLogger() + service := NewScopes(scopes, log) + + id := uuid.MustParse("10000000-1000-1000-2000-000000000001") + + tests := []struct { + name string + before func() + req *proto.CreateScopeRequest + expected *proto.CreateScopeResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + scopes.EXPECT().Create(ctx, gomock.Any()).Return(&models.Scope{ + ID: id, + Name: models.SsoServiceType, + Description: "SSO-service scope", + }, nil) + }, + req: &proto.CreateScopeRequest{ + Name: "sso-service", + Description: "SSO-service scope", + }, + expected: &proto.CreateScopeResponse{ + Data: &proto.Scope{ + Id: id.String(), + Name: "sso-service", + Description: "SSO-service scope", + }, + }, + error: false, + }, + { + name: "Internal Error", + before: func() { + scopes.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.CreateScopeRequest{ + Name: "sso-service", + Description: "SSO-service scope", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Validation Error", + req: &proto.CreateScopeRequest{ + Name: "", + Description: "SSO-service scope", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Create(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Scopes_Update(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + scopes := services.NewMockScopes(ctrl) + log := logger.NewLogger() + service := NewScopes(scopes, log) + + id := uuid.MustParse("10000000-1000-1000-2000-000000000001") + + tests := []struct { + name string + before func() + req *proto.UpdateScopeRequest + expected *proto.UpdateScopeResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + scopes.EXPECT().Update(ctx, gomock.Any()).Return(&models.Scope{ + ID: id, + Name: models.SsoServiceType, + Description: "SSO-service scope updated", + }, nil) + }, + req: &proto.UpdateScopeRequest{ + Id: id.String(), + Name: "sso-service", + Description: "SSO-service scope updated", + }, + expected: &proto.UpdateScopeResponse{ + Data: &proto.Scope{ + Id: id.String(), + Name: "sso-service", + Description: "SSO-service scope updated", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrScopeNotFound) + }, + req: &proto.UpdateScopeRequest{ + Id: id.String(), + Name: "sso-service", + Description: "SSO-service scope updated", + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.UpdateScopeRequest{ + Id: id.String(), + Name: "sso-service", + Description: "SSO-service scope updated", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.UpdateScopeRequest{ + Id: "invalid-uuid", + Name: "sso-service", + Description: "SSO-service scope updated", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Update(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Scopes_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + scopes := services.NewMockScopes(ctrl) + log := logger.NewLogger() + service := NewScopes(scopes, log) + + id := uuid.MustParse("10000000-1000-1000-2000-000000000001") + + tests := []struct { + name string + before func() + req *proto.DeleteScopeRequest + expected *emptypb.Empty + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + scopes.EXPECT().Delete(ctx, id).Return(true, nil) + }, + req: &proto.DeleteScopeRequest{ + Id: id.String(), + }, + expected: &emptypb.Empty{}, + error: false, + }, + { + name: "Not Found", + before: func() { + scopes.EXPECT().Delete(ctx, id).Return(false, errors.ErrScopeNotFound) + }, + req: &proto.DeleteScopeRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + scopes.EXPECT().Delete(ctx, id).Return(false, assert.AnError) + }, + req: &proto.DeleteScopeRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.DeleteScopeRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Delete(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} From c66bf488f0a2e57c7e1b2788e01eaf06b2608840 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 2 Apr 2025 12:03:32 +0300 Subject: [PATCH 04/20] feat(grpc) Add gRPC role service Added role service with CRUD operations for managing user roles --- internal/app/rpcs/proto/sso/v1/role.pb.go | 621 ++++++++++++++++++ .../app/rpcs/proto/sso/v1/role_grpc.pb.go | 278 ++++++++ internal/app/rpcs/registry.go | 4 + internal/app/rpcs/registry_test.go | 5 + internal/app/rpcs/services/module.go | 1 + internal/app/rpcs/services/roles.go | 202 ++++++ internal/app/rpcs/services/roles_test.go | 484 ++++++++++++++ 7 files changed, 1595 insertions(+) create mode 100644 internal/app/rpcs/proto/sso/v1/role.pb.go create mode 100644 internal/app/rpcs/proto/sso/v1/role_grpc.pb.go create mode 100644 internal/app/rpcs/services/roles.go create mode 100644 internal/app/rpcs/services/roles_test.go diff --git a/internal/app/rpcs/proto/sso/v1/role.pb.go b/internal/app/rpcs/proto/sso/v1/role.pb.go new file mode 100644 index 0000000..106a503 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/role.pb.go @@ -0,0 +1,621 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: sso/v1/role.proto + +package ssov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Role represents a role object +type Role struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + PermissionIds []string `protobuf:"bytes,4,rep,name=permission_ids,json=permissionIds,proto3" json:"permission_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Role) Reset() { + *x = Role{} + mi := &file_sso_v1_role_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Role) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Role) ProtoMessage() {} + +func (x *Role) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Role.ProtoReflect.Descriptor instead. +func (*Role) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{0} +} + +func (x *Role) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Role) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Role) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Role) GetPermissionIds() []string { + if x != nil { + return x.PermissionIds + } + return nil +} + +// ListRolesResponse is the response for the List method +type ListRolesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []*Role `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` + Meta *PaginationMeta `protobuf:"bytes,2,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRolesResponse) Reset() { + *x = ListRolesResponse{} + mi := &file_sso_v1_role_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRolesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRolesResponse) ProtoMessage() {} + +func (x *ListRolesResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRolesResponse.ProtoReflect.Descriptor instead. +func (*ListRolesResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{1} +} + +func (x *ListRolesResponse) GetData() []*Role { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListRolesResponse) GetMeta() *PaginationMeta { + if x != nil { + return x.Meta + } + return nil +} + +// GetRoleRequest is the request for the Get method +type GetRoleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRoleRequest) Reset() { + *x = GetRoleRequest{} + mi := &file_sso_v1_role_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRoleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRoleRequest) ProtoMessage() {} + +func (x *GetRoleRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRoleRequest.ProtoReflect.Descriptor instead. +func (*GetRoleRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{2} +} + +func (x *GetRoleRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetRoleResponse is the response for the Get method +type GetRoleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Role `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetRoleResponse) Reset() { + *x = GetRoleResponse{} + mi := &file_sso_v1_role_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetRoleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetRoleResponse) ProtoMessage() {} + +func (x *GetRoleResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetRoleResponse.ProtoReflect.Descriptor instead. +func (*GetRoleResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{3} +} + +func (x *GetRoleResponse) GetData() *Role { + if x != nil { + return x.Data + } + return nil +} + +// CreateRoleRequest is the request for the Create method +type CreateRoleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + PermissionIds []string `protobuf:"bytes,3,rep,name=permission_ids,json=permissionIds,proto3" json:"permission_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRoleRequest) Reset() { + *x = CreateRoleRequest{} + mi := &file_sso_v1_role_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRoleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRoleRequest) ProtoMessage() {} + +func (x *CreateRoleRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRoleRequest.ProtoReflect.Descriptor instead. +func (*CreateRoleRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateRoleRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateRoleRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *CreateRoleRequest) GetPermissionIds() []string { + if x != nil { + return x.PermissionIds + } + return nil +} + +// CreateRoleResponse is the response for the Create method +type CreateRoleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Role `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateRoleResponse) Reset() { + *x = CreateRoleResponse{} + mi := &file_sso_v1_role_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateRoleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateRoleResponse) ProtoMessage() {} + +func (x *CreateRoleResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateRoleResponse.ProtoReflect.Descriptor instead. +func (*CreateRoleResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateRoleResponse) GetData() *Role { + if x != nil { + return x.Data + } + return nil +} + +// UpdateRoleRequest is the request for the Update method +type UpdateRoleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + PermissionIds []string `protobuf:"bytes,4,rep,name=permission_ids,json=permissionIds,proto3" json:"permission_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRoleRequest) Reset() { + *x = UpdateRoleRequest{} + mi := &file_sso_v1_role_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRoleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRoleRequest) ProtoMessage() {} + +func (x *UpdateRoleRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRoleRequest.ProtoReflect.Descriptor instead. +func (*UpdateRoleRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateRoleRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateRoleRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpdateRoleRequest) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *UpdateRoleRequest) GetPermissionIds() []string { + if x != nil { + return x.PermissionIds + } + return nil +} + +// UpdateRoleResponse is the response for the Update method +type UpdateRoleResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *Role `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateRoleResponse) Reset() { + *x = UpdateRoleResponse{} + mi := &file_sso_v1_role_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateRoleResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateRoleResponse) ProtoMessage() {} + +func (x *UpdateRoleResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateRoleResponse.ProtoReflect.Descriptor instead. +func (*UpdateRoleResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateRoleResponse) GetData() *Role { + if x != nil { + return x.Data + } + return nil +} + +// DeleteRoleRequest is the request for the Delete method +type DeleteRoleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRoleRequest) Reset() { + *x = DeleteRoleRequest{} + mi := &file_sso_v1_role_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRoleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRoleRequest) ProtoMessage() {} + +func (x *DeleteRoleRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_role_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRoleRequest.ProtoReflect.Descriptor instead. +func (*DeleteRoleRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_role_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteRoleRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_sso_v1_role_proto protoreflect.FileDescriptor + +const file_sso_v1_role_proto_rawDesc = "" + + "\n" + + "\x11sso/v1/role.proto\x12\x06sso.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x17sso/v1/pagination.proto\"\xa3\x01\n" + + "\x04Role\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1d\n" + + "\x04name\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\x124\n" + + "\x0epermission_ids\x18\x04 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\rpermissionIds\"a\n" + + "\x11ListRolesResponse\x12 \n" + + "\x04data\x18\x01 \x03(\v2\f.sso.v1.RoleR\x04data\x12*\n" + + "\x04meta\x18\x02 \x01(\v2\x16.sso.v1.PaginationMetaR\x04meta\"*\n" + + "\x0eGetRoleRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"3\n" + + "\x0fGetRoleResponse\x12 \n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.RoleR\x04data\"\x96\x01\n" + + "\x11CreateRoleRequest\x12\x1d\n" + + "\x04name\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x02 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\x124\n" + + "\x0epermission_ids\x18\x03 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\rpermissionIds\"6\n" + + "\x12CreateRoleResponse\x12 \n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.RoleR\x04data\"\xb0\x01\n" + + "\x11UpdateRoleRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12\x1d\n" + + "\x04name\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x18dR\x04name\x12,\n" + + "\vdescription\x18\x03 \x01(\tB\n" + + "\xbaH\ar\x05\x10\x01\x18\x80\x10R\vdescription\x124\n" + + "\x0epermission_ids\x18\x04 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\rpermissionIds\"6\n" + + "\x12UpdateRoleResponse\x12 \n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.RoleR\x04data\"-\n" + + "\x11DeleteRoleRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id2\xcf\x02\n" + + "\vRoleService\x12A\n" + + "\x04List\x12\x1c.sso.v1.PaginatedListRequest\x1a\x19.sso.v1.ListRolesResponse\"\x00\x128\n" + + "\x03Get\x12\x16.sso.v1.GetRoleRequest\x1a\x17.sso.v1.GetRoleResponse\"\x00\x12A\n" + + "\x06Create\x12\x19.sso.v1.CreateRoleRequest\x1a\x1a.sso.v1.CreateRoleResponse\"\x00\x12A\n" + + "\x06Update\x12\x19.sso.v1.UpdateRoleRequest\x1a\x1a.sso.v1.UpdateRoleResponse\"\x00\x12=\n" + + "\x06Delete\x12\x19.sso.v1.DeleteRoleRequest\x1a\x16.google.protobuf.Empty\"\x00B+Z)loki/internal/app/rpcs/proto/sso/v1;ssov1b\x06proto3" + +var ( + file_sso_v1_role_proto_rawDescOnce sync.Once + file_sso_v1_role_proto_rawDescData []byte +) + +func file_sso_v1_role_proto_rawDescGZIP() []byte { + file_sso_v1_role_proto_rawDescOnce.Do(func() { + file_sso_v1_role_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sso_v1_role_proto_rawDesc), len(file_sso_v1_role_proto_rawDesc))) + }) + return file_sso_v1_role_proto_rawDescData +} + +var file_sso_v1_role_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_sso_v1_role_proto_goTypes = []any{ + (*Role)(nil), // 0: sso.v1.Role + (*ListRolesResponse)(nil), // 1: sso.v1.ListRolesResponse + (*GetRoleRequest)(nil), // 2: sso.v1.GetRoleRequest + (*GetRoleResponse)(nil), // 3: sso.v1.GetRoleResponse + (*CreateRoleRequest)(nil), // 4: sso.v1.CreateRoleRequest + (*CreateRoleResponse)(nil), // 5: sso.v1.CreateRoleResponse + (*UpdateRoleRequest)(nil), // 6: sso.v1.UpdateRoleRequest + (*UpdateRoleResponse)(nil), // 7: sso.v1.UpdateRoleResponse + (*DeleteRoleRequest)(nil), // 8: sso.v1.DeleteRoleRequest + (*PaginationMeta)(nil), // 9: sso.v1.PaginationMeta + (*PaginatedListRequest)(nil), // 10: sso.v1.PaginatedListRequest + (*emptypb.Empty)(nil), // 11: google.protobuf.Empty +} +var file_sso_v1_role_proto_depIdxs = []int32{ + 0, // 0: sso.v1.ListRolesResponse.data:type_name -> sso.v1.Role + 9, // 1: sso.v1.ListRolesResponse.meta:type_name -> sso.v1.PaginationMeta + 0, // 2: sso.v1.GetRoleResponse.data:type_name -> sso.v1.Role + 0, // 3: sso.v1.CreateRoleResponse.data:type_name -> sso.v1.Role + 0, // 4: sso.v1.UpdateRoleResponse.data:type_name -> sso.v1.Role + 10, // 5: sso.v1.RoleService.List:input_type -> sso.v1.PaginatedListRequest + 2, // 6: sso.v1.RoleService.Get:input_type -> sso.v1.GetRoleRequest + 4, // 7: sso.v1.RoleService.Create:input_type -> sso.v1.CreateRoleRequest + 6, // 8: sso.v1.RoleService.Update:input_type -> sso.v1.UpdateRoleRequest + 8, // 9: sso.v1.RoleService.Delete:input_type -> sso.v1.DeleteRoleRequest + 1, // 10: sso.v1.RoleService.List:output_type -> sso.v1.ListRolesResponse + 3, // 11: sso.v1.RoleService.Get:output_type -> sso.v1.GetRoleResponse + 5, // 12: sso.v1.RoleService.Create:output_type -> sso.v1.CreateRoleResponse + 7, // 13: sso.v1.RoleService.Update:output_type -> sso.v1.UpdateRoleResponse + 11, // 14: sso.v1.RoleService.Delete:output_type -> google.protobuf.Empty + 10, // [10:15] is the sub-list for method output_type + 5, // [5:10] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_sso_v1_role_proto_init() } +func file_sso_v1_role_proto_init() { + if File_sso_v1_role_proto != nil { + return + } + file_sso_v1_pagination_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sso_v1_role_proto_rawDesc), len(file_sso_v1_role_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sso_v1_role_proto_goTypes, + DependencyIndexes: file_sso_v1_role_proto_depIdxs, + MessageInfos: file_sso_v1_role_proto_msgTypes, + }.Build() + File_sso_v1_role_proto = out.File + file_sso_v1_role_proto_goTypes = nil + file_sso_v1_role_proto_depIdxs = nil +} diff --git a/internal/app/rpcs/proto/sso/v1/role_grpc.pb.go b/internal/app/rpcs/proto/sso/v1/role_grpc.pb.go new file mode 100644 index 0000000..e0f845e --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/role_grpc.pb.go @@ -0,0 +1,278 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: sso/v1/role.proto + +package ssov1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RoleService_List_FullMethodName = "/sso.v1.RoleService/List" + RoleService_Get_FullMethodName = "/sso.v1.RoleService/Get" + RoleService_Create_FullMethodName = "/sso.v1.RoleService/Create" + RoleService_Update_FullMethodName = "/sso.v1.RoleService/Update" + RoleService_Delete_FullMethodName = "/sso.v1.RoleService/Delete" +) + +// RoleServiceClient is the client API for RoleService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Role service provides CRUD operations for managing user roles +type RoleServiceClient interface { + List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListRolesResponse, error) + Get(ctx context.Context, in *GetRoleRequest, opts ...grpc.CallOption) (*GetRoleResponse, error) + Create(ctx context.Context, in *CreateRoleRequest, opts ...grpc.CallOption) (*CreateRoleResponse, error) + Update(ctx context.Context, in *UpdateRoleRequest, opts ...grpc.CallOption) (*UpdateRoleResponse, error) + Delete(ctx context.Context, in *DeleteRoleRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type roleServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRoleServiceClient(cc grpc.ClientConnInterface) RoleServiceClient { + return &roleServiceClient{cc} +} + +func (c *roleServiceClient) List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListRolesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRolesResponse) + err := c.cc.Invoke(ctx, RoleService_List_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *roleServiceClient) Get(ctx context.Context, in *GetRoleRequest, opts ...grpc.CallOption) (*GetRoleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetRoleResponse) + err := c.cc.Invoke(ctx, RoleService_Get_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *roleServiceClient) Create(ctx context.Context, in *CreateRoleRequest, opts ...grpc.CallOption) (*CreateRoleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateRoleResponse) + err := c.cc.Invoke(ctx, RoleService_Create_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *roleServiceClient) Update(ctx context.Context, in *UpdateRoleRequest, opts ...grpc.CallOption) (*UpdateRoleResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateRoleResponse) + err := c.cc.Invoke(ctx, RoleService_Update_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *roleServiceClient) Delete(ctx context.Context, in *DeleteRoleRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, RoleService_Delete_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RoleServiceServer is the server API for RoleService service. +// All implementations must embed UnimplementedRoleServiceServer +// for forward compatibility. +// +// Role service provides CRUD operations for managing user roles +type RoleServiceServer interface { + List(context.Context, *PaginatedListRequest) (*ListRolesResponse, error) + Get(context.Context, *GetRoleRequest) (*GetRoleResponse, error) + Create(context.Context, *CreateRoleRequest) (*CreateRoleResponse, error) + Update(context.Context, *UpdateRoleRequest) (*UpdateRoleResponse, error) + Delete(context.Context, *DeleteRoleRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedRoleServiceServer() +} + +// UnimplementedRoleServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRoleServiceServer struct{} + +func (UnimplementedRoleServiceServer) List(context.Context, *PaginatedListRequest) (*ListRolesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedRoleServiceServer) Get(context.Context, *GetRoleRequest) (*GetRoleResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (UnimplementedRoleServiceServer) Create(context.Context, *CreateRoleRequest) (*CreateRoleResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") +} +func (UnimplementedRoleServiceServer) Update(context.Context, *UpdateRoleRequest) (*UpdateRoleResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (UnimplementedRoleServiceServer) Delete(context.Context, *DeleteRoleRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedRoleServiceServer) mustEmbedUnimplementedRoleServiceServer() {} +func (UnimplementedRoleServiceServer) testEmbeddedByValue() {} + +// UnsafeRoleServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RoleServiceServer will +// result in compilation errors. +type UnsafeRoleServiceServer interface { + mustEmbedUnimplementedRoleServiceServer() +} + +func RegisterRoleServiceServer(s grpc.ServiceRegistrar, srv RoleServiceServer) { + // If the following call pancis, it indicates UnimplementedRoleServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RoleService_ServiceDesc, srv) +} + +func _RoleService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PaginatedListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoleServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoleService_List_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoleServiceServer).List(ctx, req.(*PaginatedListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoleService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoleServiceServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoleService_Get_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoleServiceServer).Get(ctx, req.(*GetRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoleService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoleServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoleService_Create_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoleServiceServer).Create(ctx, req.(*CreateRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoleService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoleServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoleService_Update_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoleServiceServer).Update(ctx, req.(*UpdateRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoleService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteRoleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoleServiceServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoleService_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoleServiceServer).Delete(ctx, req.(*DeleteRoleRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RoleService_ServiceDesc is the grpc.ServiceDesc for RoleService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RoleService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sso.v1.RoleService", + HandlerType: (*RoleServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _RoleService_List_Handler, + }, + { + MethodName: "Get", + Handler: _RoleService_Get_Handler, + }, + { + MethodName: "Create", + Handler: _RoleService_Create_Handler, + }, + { + MethodName: "Update", + Handler: _RoleService_Update_Handler, + }, + { + MethodName: "Delete", + Handler: _RoleService_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sso/v1/role.proto", +} diff --git a/internal/app/rpcs/registry.go b/internal/app/rpcs/registry.go index e148819..75eb5c8 100644 --- a/internal/app/rpcs/registry.go +++ b/internal/app/rpcs/registry.go @@ -8,20 +8,24 @@ import ( type Registry struct { permissions proto.PermissionServiceServer + roles proto.RoleServiceServer scopes proto.ScopeServiceServer } func NewRegistry( permissions proto.PermissionServiceServer, + roles proto.RoleServiceServer, scopes proto.ScopeServiceServer, ) *Registry { return &Registry{ permissions: permissions, + roles: roles, scopes: scopes, } } func (r *Registry) RegisterAll(server *grpc.Server) { proto.RegisterPermissionServiceServer(server, r.permissions) + proto.RegisterRoleServiceServer(server, r.roles) proto.RegisterScopeServiceServer(server, r.scopes) } diff --git a/internal/app/rpcs/registry_test.go b/internal/app/rpcs/registry_test.go index b54e556..13e6960 100644 --- a/internal/app/rpcs/registry_test.go +++ b/internal/app/rpcs/registry_test.go @@ -13,6 +13,10 @@ type permissionService struct { proto.UnimplementedPermissionServiceServer } +type roleService struct { + proto.UnimplementedRoleServiceServer +} + type scopeService struct { proto.UnimplementedScopeServiceServer } @@ -20,6 +24,7 @@ type scopeService struct { func Test_Registry_RegisterAll(t *testing.T) { registry := NewRegistry( &permissionService{}, + &roleService{}, &scopeService{}, ) assert.NotNil(t, registry) diff --git a/internal/app/rpcs/services/module.go b/internal/app/rpcs/services/module.go index cb6e89d..e3c53d2 100644 --- a/internal/app/rpcs/services/module.go +++ b/internal/app/rpcs/services/module.go @@ -4,5 +4,6 @@ import "go.uber.org/fx" var Module = fx.Options( fx.Provide(NewPermissions), + fx.Provide(NewRoles), fx.Provide(NewScopes), ) diff --git a/internal/app/rpcs/services/roles.go b/internal/app/rpcs/services/roles.go new file mode 100644 index 0000000..a14ed9a --- /dev/null +++ b/internal/app/rpcs/services/roles.go @@ -0,0 +1,202 @@ +package services + +import ( + "context" + + "github.com/bufbuild/protovalidate-go" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +type rolesService struct { + proto.UnimplementedRoleServiceServer + roles services.Roles + log *logger.Logger +} + +func NewRoles(roles services.Roles, log *logger.Logger) proto.RoleServiceServer { + return &rolesService{ + roles: roles, + log: log, + } +} + +func (p *rolesService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListRolesResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + } + + pagination := &services.Pagination{ + Page: req.Limit, + PerPage: req.Offset, + } + + rows, total, err := p.roles.List(ctx, pagination) + if err != nil { + p.log.Error().Err(err).Msg("Failed to fetch roles") + return nil, status.Error(codes.Internal, "failed to fetch roles") + } + + collection := make([]*proto.Role, 0, len(rows)) + for _, row := range rows { + collection = append(collection, &proto.Role{ + Id: row.ID.String(), + Name: row.Name, + Description: row.Description, + }) + } + + return &proto.ListRolesResponse{ + Data: collection, + Meta: &proto.PaginationMeta{ + Page: pagination.Page, + Per: pagination.PerPage, + Total: total, + }, + }, nil +} + +func (p *rolesService) Get(ctx context.Context, req *proto.GetRoleRequest) (*proto.GetRoleResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse role ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + role, err := p.roles.FindById(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get role") + if errors.Is(err, errors.ErrRoleNotFound) { + return nil, status.Error(codes.NotFound, "role not found") + } + return nil, status.Error(codes.Internal, "failed to get role") + } + + return &proto.GetRoleResponse{ + Data: &proto.Role{ + Id: role.ID.String(), + Name: role.Name, + Description: role.Description, + PermissionIds: []string{}, + }, + }, nil +} + +func (p *rolesService) Create(ctx context.Context, req *proto.CreateRoleRequest) (*proto.CreateRoleResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + permissionIDs := make([]uuid.UUID, 0, len(req.PermissionIds)) + for _, permissionId := range req.PermissionIds { + id, err := uuid.Parse(permissionId) + if err != nil { + p.log.Error().Err(err).Str("permission_id", permissionId).Msg("Invalid permission ID format") + return nil, status.Error(codes.InvalidArgument, "invalid permission ID format") + } + permissionIDs = append(permissionIDs, id) + } + + role, err := p.roles.Create(ctx, &models.Role{ + Name: req.Name, + Description: req.Description, + PermissionIDs: permissionIDs, + }) + if err != nil { + p.log.Error().Err(err).Str("name", req.Name).Msg("Failed to create role") + return nil, status.Error(codes.Internal, err.Error()) + } + + return &proto.CreateRoleResponse{ + Data: &proto.Role{ + Id: role.ID.String(), + Name: role.Name, + Description: role.Description, + }, + }, nil +} + +func (p *rolesService) Update(ctx context.Context, req *proto.UpdateRoleRequest) (*proto.UpdateRoleResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + permissionIDs := make([]uuid.UUID, 0, len(req.PermissionIds)) + for _, permissionId := range req.PermissionIds { + id, err := uuid.Parse(permissionId) + if err != nil { + p.log.Error().Err(err).Str("permission_id", permissionId).Msg("Invalid permission ID format") + return nil, status.Error(codes.InvalidArgument, "invalid permission ID format") + } + permissionIDs = append(permissionIDs, id) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Invalid UUID format") + return nil, status.Error(codes.InvalidArgument, "invalid role id format") + } + + role, err := p.roles.Update(ctx, &models.Role{ + ID: id, + Name: req.Name, + Description: req.Description, + PermissionIDs: permissionIDs, + }) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update role") + + switch { + case errors.Is(err, errors.ErrRoleNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to update role") + } + } + + return &proto.UpdateRoleResponse{ + Data: &proto.Role{ + Id: role.ID.String(), + Name: role.Name, + Description: role.Description, + }, + }, nil +} + +func (p *rolesService) Delete(ctx context.Context, req *proto.DeleteRoleRequest) (*emptypb.Empty, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse role ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + _, err = p.roles.Delete(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete role") + + switch { + case errors.Is(err, errors.ErrRoleNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to delete role") + } + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/app/rpcs/services/roles_test.go b/internal/app/rpcs/services/roles_test.go new file mode 100644 index 0000000..dacd9d0 --- /dev/null +++ b/internal/app/rpcs/services/roles_test.go @@ -0,0 +1,484 @@ +package services + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +func Test_Roles_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + roles := services.NewMockRoles(ctrl) + log := logger.NewLogger() + service := NewRoles(roles, log) + + tests := []struct { + name string + before func() + request *proto.PaginatedListRequest + expected *proto.ListRolesResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + roles.EXPECT().List(ctx, gomock.Any()).Return([]models.Role{ + { + ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), + Name: models.AdminRoleType, + Description: "Admin role", + }, + { + ID: uuid.MustParse("10000000-1000-1000-1000-000000000002"), + Name: models.ManagerRoleType, + Description: "Manager role", + }, + { + ID: uuid.MustParse("10000000-1000-1000-1000-000000000003"), + Name: models.UserRoleType, + Description: "User role", + }, + }, uint64(3), nil) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: &proto.ListRolesResponse{ + Data: []*proto.Role{ + { + Id: "10000000-1000-1000-1000-000000000001", + Name: "admin", + Description: "Admin role", + }, + { + Id: "10000000-1000-1000-1000-000000000002", + Name: "manager", + Description: "Manager role", + }, + { + Id: "10000000-1000-1000-1000-000000000003", + Name: "user", + Description: "User role", + }, + }, + Meta: &proto.PaginationMeta{ + Page: 1, + Per: 10, + Total: 3, + }, + }, + error: false, + }, + { + name: "Error", + before: func() { + roles.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, + code: codes.Internal, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.List(ctx, tt.request) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.expected.Data), len(result.Data)) + assert.Equal(t, tt.expected.Meta.Total, result.Meta.Total) + for i, role := range tt.expected.Data { + assert.Equal(t, role.Id, result.Data[i].Id) + assert.Equal(t, role.Name, result.Data[i].Name) + assert.Equal(t, role.Description, result.Data[i].Description) + } + } + }) + } +} + +func Test_Roles_Get(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + roles := services.NewMockRoles(ctrl) + log := logger.NewLogger() + service := NewRoles(roles, log) + + id := uuid.MustParse("10000000-1000-1000-1000-000000000001") + + tests := []struct { + name string + before func() + req *proto.GetRoleRequest + expected *proto.GetRoleResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + roles.EXPECT().FindById(ctx, id).Return(&models.Role{ + ID: id, + Name: models.AdminRoleType, + Description: "Admin role", + }, nil) + }, + req: &proto.GetRoleRequest{ + Id: id.String(), + }, + expected: &proto.GetRoleResponse{ + Data: &proto.Role{ + Id: id.String(), + Name: "admin", + Description: "Admin role", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + roles.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRoleNotFound) + }, + req: &proto.GetRoleRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.GetRoleRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Get(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Roles_Create(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + roles := services.NewMockRoles(ctrl) + log := logger.NewLogger() + service := NewRoles(roles, log) + + id := uuid.MustParse("10000000-1000-1000-1000-000000000001") + + tests := []struct { + name string + before func() + req *proto.CreateRoleRequest + expected *proto.CreateRoleResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + roles.EXPECT().Create(ctx, gomock.Any()).Return(&models.Role{ + ID: id, + Name: models.AdminRoleType, + Description: "Admin role", + }, nil) + }, + req: &proto.CreateRoleRequest{ + Name: "admin", + Description: "Admin role", + }, + expected: &proto.CreateRoleResponse{ + Data: &proto.Role{ + Id: id.String(), + Name: "admin", + Description: "Admin role", + }, + }, + error: false, + }, + { + name: "Internal Error", + before: func() { + roles.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.CreateRoleRequest{ + Name: "admin", + Description: "Admin role", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Validation Error", + req: &proto.CreateRoleRequest{ + Name: "", + Description: "Admin role", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Create(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Roles_Update(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + roles := services.NewMockRoles(ctrl) + log := logger.NewLogger() + service := NewRoles(roles, log) + + id := uuid.MustParse("10000000-1000-1000-1000-000000000001") + + tests := []struct { + name string + before func() + req *proto.UpdateRoleRequest + expected *proto.UpdateRoleResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + roles.EXPECT().Update(ctx, gomock.Any()).Return(&models.Role{ + ID: id, + Name: models.AdminRoleType, + Description: "Admin role updated", + }, nil) + }, + req: &proto.UpdateRoleRequest{ + Id: id.String(), + Name: "admin", + Description: "Admin role updated", + }, + expected: &proto.UpdateRoleResponse{ + Data: &proto.Role{ + Id: id.String(), + Name: "admin", + Description: "Admin role updated", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrRoleNotFound) + }, + req: &proto.UpdateRoleRequest{ + Id: id.String(), + Name: "admin", + Description: "Admin role updated", + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.UpdateRoleRequest{ + Id: id.String(), + Name: "admin", + Description: "Admin role updated", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.UpdateRoleRequest{ + Id: "invalid-uuid", + Name: "admin", + Description: "Admin role updated", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Update(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.Name, result.Data.Name) + assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + } + }) + } +} + +func Test_Roles_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + roles := services.NewMockRoles(ctrl) + log := logger.NewLogger() + service := NewRoles(roles, log) + + id := uuid.MustParse("10000000-1000-1000-1000-000000000001") + + tests := []struct { + name string + before func() + req *proto.DeleteRoleRequest + expected *emptypb.Empty + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + roles.EXPECT().Delete(ctx, id).Return(true, nil) + }, + req: &proto.DeleteRoleRequest{ + Id: id.String(), + }, + expected: &emptypb.Empty{}, + error: false, + }, + { + name: "Not Found", + before: func() { + roles.EXPECT().Delete(ctx, id).Return(false, errors.ErrRoleNotFound) + }, + req: &proto.DeleteRoleRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + roles.EXPECT().Delete(ctx, id).Return(false, assert.AnError) + }, + req: &proto.DeleteRoleRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.DeleteRoleRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Delete(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} From c9baa3ebedfef3ab653c8ac74d7ccfa1195db4b6 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 2 Apr 2025 14:57:51 +0300 Subject: [PATCH 05/20] feat(grpc) Add gRPC token service Added token service with List and Delete operations for managing user JSON web tokens --- internal/app/errors/errors.go | 3 + internal/app/rpcs/proto/sso/v1/token.pb.go | 283 ++++++++++++++++++ .../app/rpcs/proto/sso/v1/token_grpc.pb.go | 164 ++++++++++ internal/app/rpcs/registry.go | 4 + internal/app/rpcs/registry_test.go | 5 + internal/app/rpcs/services/module.go | 1 + internal/app/rpcs/services/permissions.go | 2 + internal/app/rpcs/services/roles.go | 2 + internal/app/rpcs/services/scopes.go | 2 + internal/app/rpcs/services/tokens.go | 95 ++++++ internal/app/rpcs/services/tokens_test.go | 204 +++++++++++++ 11 files changed, 765 insertions(+) create mode 100644 internal/app/rpcs/proto/sso/v1/token.pb.go create mode 100644 internal/app/rpcs/proto/sso/v1/token_grpc.pb.go create mode 100644 internal/app/rpcs/services/tokens.go create mode 100644 internal/app/rpcs/services/tokens_test.go diff --git a/internal/app/errors/errors.go b/internal/app/errors/errors.go index d7b5115..a5ec495 100644 --- a/internal/app/errors/errors.go +++ b/internal/app/errors/errors.go @@ -60,6 +60,9 @@ var ( // ErrScopeNotFound indicates that the requested scope could not be found ErrScopeNotFound = errors.New("scope not found") + // ErrTokenNotFound indicates that the requested JSON web token could not be found + ErrTokenNotFound = errors.New("token not found") + // ErrUserNotFound indicates that the requested user could not be found ErrUserNotFound = errors.New("user not found") diff --git a/internal/app/rpcs/proto/sso/v1/token.pb.go b/internal/app/rpcs/proto/sso/v1/token.pb.go new file mode 100644 index 0000000..1f94130 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/token.pb.go @@ -0,0 +1,283 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: sso/v1/token.proto + +package ssov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Token represents a JWT-token object +type Token struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Token) Reset() { + *x = Token{} + mi := &file_sso_v1_token_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Token) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Token) ProtoMessage() {} + +func (x *Token) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_token_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Token.ProtoReflect.Descriptor instead. +func (*Token) Descriptor() ([]byte, []int) { + return file_sso_v1_token_proto_rawDescGZIP(), []int{0} +} + +func (x *Token) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Token) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *Token) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Token) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *Token) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +// ListTokensResponse is the response for the List method +type ListTokensResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []*Token `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` + Meta *PaginationMeta `protobuf:"bytes,2,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListTokensResponse) Reset() { + *x = ListTokensResponse{} + mi := &file_sso_v1_token_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListTokensResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListTokensResponse) ProtoMessage() {} + +func (x *ListTokensResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_token_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListTokensResponse.ProtoReflect.Descriptor instead. +func (*ListTokensResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_token_proto_rawDescGZIP(), []int{1} +} + +func (x *ListTokensResponse) GetData() []*Token { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListTokensResponse) GetMeta() *PaginationMeta { + if x != nil { + return x.Meta + } + return nil +} + +// DeleteTokenRequest is the request for the Delete method +type DeleteTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteTokenRequest) Reset() { + *x = DeleteTokenRequest{} + mi := &file_sso_v1_token_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteTokenRequest) ProtoMessage() {} + +func (x *DeleteTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_token_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteTokenRequest.ProtoReflect.Descriptor instead. +func (*DeleteTokenRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_token_proto_rawDescGZIP(), []int{2} +} + +func (x *DeleteTokenRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_sso_v1_token_proto protoreflect.FileDescriptor + +const file_sso_v1_token_proto_rawDesc = "" + + "\n" + + "\x12sso/v1/token.proto\x12\x06sso.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x17sso/v1/pagination.proto\"\xd6\x01\n" + + "\x05Token\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x12!\n" + + "\auser_id\x18\x02 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x06userId\x126\n" + + "\x04type\x18\x03 \x01(\tB\"\xbaH\x1fr\x1dR\faccess_tokenR\rrefresh_tokenR\x04type\x12\x1d\n" + + "\x05value\x18\x04 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\x05value\x129\n" + + "\n" + + "expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\texpiresAt\"c\n" + + "\x12ListTokensResponse\x12!\n" + + "\x04data\x18\x01 \x03(\v2\r.sso.v1.TokenR\x04data\x12*\n" + + "\x04meta\x18\x02 \x01(\v2\x16.sso.v1.PaginationMetaR\x04meta\".\n" + + "\x12DeleteTokenRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id2\x92\x01\n" + + "\fTokenService\x12B\n" + + "\x04List\x12\x1c.sso.v1.PaginatedListRequest\x1a\x1a.sso.v1.ListTokensResponse\"\x00\x12>\n" + + "\x06Delete\x12\x1a.sso.v1.DeleteTokenRequest\x1a\x16.google.protobuf.Empty\"\x00B+Z)loki/internal/app/rpcs/proto/sso/v1;ssov1b\x06proto3" + +var ( + file_sso_v1_token_proto_rawDescOnce sync.Once + file_sso_v1_token_proto_rawDescData []byte +) + +func file_sso_v1_token_proto_rawDescGZIP() []byte { + file_sso_v1_token_proto_rawDescOnce.Do(func() { + file_sso_v1_token_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sso_v1_token_proto_rawDesc), len(file_sso_v1_token_proto_rawDesc))) + }) + return file_sso_v1_token_proto_rawDescData +} + +var file_sso_v1_token_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_sso_v1_token_proto_goTypes = []any{ + (*Token)(nil), // 0: sso.v1.Token + (*ListTokensResponse)(nil), // 1: sso.v1.ListTokensResponse + (*DeleteTokenRequest)(nil), // 2: sso.v1.DeleteTokenRequest + (*timestamppb.Timestamp)(nil), // 3: google.protobuf.Timestamp + (*PaginationMeta)(nil), // 4: sso.v1.PaginationMeta + (*PaginatedListRequest)(nil), // 5: sso.v1.PaginatedListRequest + (*emptypb.Empty)(nil), // 6: google.protobuf.Empty +} +var file_sso_v1_token_proto_depIdxs = []int32{ + 3, // 0: sso.v1.Token.expires_at:type_name -> google.protobuf.Timestamp + 0, // 1: sso.v1.ListTokensResponse.data:type_name -> sso.v1.Token + 4, // 2: sso.v1.ListTokensResponse.meta:type_name -> sso.v1.PaginationMeta + 5, // 3: sso.v1.TokenService.List:input_type -> sso.v1.PaginatedListRequest + 2, // 4: sso.v1.TokenService.Delete:input_type -> sso.v1.DeleteTokenRequest + 1, // 5: sso.v1.TokenService.List:output_type -> sso.v1.ListTokensResponse + 6, // 6: sso.v1.TokenService.Delete:output_type -> google.protobuf.Empty + 5, // [5:7] is the sub-list for method output_type + 3, // [3:5] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_sso_v1_token_proto_init() } +func file_sso_v1_token_proto_init() { + if File_sso_v1_token_proto != nil { + return + } + file_sso_v1_pagination_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sso_v1_token_proto_rawDesc), len(file_sso_v1_token_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sso_v1_token_proto_goTypes, + DependencyIndexes: file_sso_v1_token_proto_depIdxs, + MessageInfos: file_sso_v1_token_proto_msgTypes, + }.Build() + File_sso_v1_token_proto = out.File + file_sso_v1_token_proto_goTypes = nil + file_sso_v1_token_proto_depIdxs = nil +} diff --git a/internal/app/rpcs/proto/sso/v1/token_grpc.pb.go b/internal/app/rpcs/proto/sso/v1/token_grpc.pb.go new file mode 100644 index 0000000..9af6ca0 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/token_grpc.pb.go @@ -0,0 +1,164 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: sso/v1/token.proto + +package ssov1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + TokenService_List_FullMethodName = "/sso.v1.TokenService/List" + TokenService_Delete_FullMethodName = "/sso.v1.TokenService/Delete" +) + +// TokenServiceClient is the client API for TokenService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Token service provides CRUD operations for managing user tokens +type TokenServiceClient interface { + List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListTokensResponse, error) + Delete(ctx context.Context, in *DeleteTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type tokenServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTokenServiceClient(cc grpc.ClientConnInterface) TokenServiceClient { + return &tokenServiceClient{cc} +} + +func (c *tokenServiceClient) List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListTokensResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListTokensResponse) + err := c.cc.Invoke(ctx, TokenService_List_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *tokenServiceClient) Delete(ctx context.Context, in *DeleteTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, TokenService_Delete_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TokenServiceServer is the server API for TokenService service. +// All implementations must embed UnimplementedTokenServiceServer +// for forward compatibility. +// +// Token service provides CRUD operations for managing user tokens +type TokenServiceServer interface { + List(context.Context, *PaginatedListRequest) (*ListTokensResponse, error) + Delete(context.Context, *DeleteTokenRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedTokenServiceServer() +} + +// UnimplementedTokenServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedTokenServiceServer struct{} + +func (UnimplementedTokenServiceServer) List(context.Context, *PaginatedListRequest) (*ListTokensResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedTokenServiceServer) Delete(context.Context, *DeleteTokenRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedTokenServiceServer) mustEmbedUnimplementedTokenServiceServer() {} +func (UnimplementedTokenServiceServer) testEmbeddedByValue() {} + +// UnsafeTokenServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TokenServiceServer will +// result in compilation errors. +type UnsafeTokenServiceServer interface { + mustEmbedUnimplementedTokenServiceServer() +} + +func RegisterTokenServiceServer(s grpc.ServiceRegistrar, srv TokenServiceServer) { + // If the following call pancis, it indicates UnimplementedTokenServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&TokenService_ServiceDesc, srv) +} + +func _TokenService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PaginatedListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TokenServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TokenService_List_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TokenServiceServer).List(ctx, req.(*PaginatedListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TokenService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TokenServiceServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: TokenService_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TokenServiceServer).Delete(ctx, req.(*DeleteTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// TokenService_ServiceDesc is the grpc.ServiceDesc for TokenService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TokenService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sso.v1.TokenService", + HandlerType: (*TokenServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _TokenService_List_Handler, + }, + { + MethodName: "Delete", + Handler: _TokenService_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sso/v1/token.proto", +} diff --git a/internal/app/rpcs/registry.go b/internal/app/rpcs/registry.go index 75eb5c8..88f86c1 100644 --- a/internal/app/rpcs/registry.go +++ b/internal/app/rpcs/registry.go @@ -10,17 +10,20 @@ type Registry struct { permissions proto.PermissionServiceServer roles proto.RoleServiceServer scopes proto.ScopeServiceServer + tokens proto.TokenServiceServer } func NewRegistry( permissions proto.PermissionServiceServer, roles proto.RoleServiceServer, scopes proto.ScopeServiceServer, + tokens proto.TokenServiceServer, ) *Registry { return &Registry{ permissions: permissions, roles: roles, scopes: scopes, + tokens: tokens, } } @@ -28,4 +31,5 @@ func (r *Registry) RegisterAll(server *grpc.Server) { proto.RegisterPermissionServiceServer(server, r.permissions) proto.RegisterRoleServiceServer(server, r.roles) proto.RegisterScopeServiceServer(server, r.scopes) + proto.RegisterTokenServiceServer(server, r.tokens) } diff --git a/internal/app/rpcs/registry_test.go b/internal/app/rpcs/registry_test.go index 13e6960..5afb0dc 100644 --- a/internal/app/rpcs/registry_test.go +++ b/internal/app/rpcs/registry_test.go @@ -21,11 +21,16 @@ type scopeService struct { proto.UnimplementedScopeServiceServer } +type tokenService struct { + proto.UnimplementedTokenServiceServer +} + func Test_Registry_RegisterAll(t *testing.T) { registry := NewRegistry( &permissionService{}, &roleService{}, &scopeService{}, + &tokenService{}, ) assert.NotNil(t, registry) diff --git a/internal/app/rpcs/services/module.go b/internal/app/rpcs/services/module.go index e3c53d2..4ce9ead 100644 --- a/internal/app/rpcs/services/module.go +++ b/internal/app/rpcs/services/module.go @@ -6,4 +6,5 @@ var Module = fx.Options( fx.Provide(NewPermissions), fx.Provide(NewRoles), fx.Provide(NewScopes), + fx.Provide(NewTokens), ) diff --git a/internal/app/rpcs/services/permissions.go b/internal/app/rpcs/services/permissions.go index cafca05..2b54c0c 100644 --- a/internal/app/rpcs/services/permissions.go +++ b/internal/app/rpcs/services/permissions.go @@ -29,6 +29,7 @@ func NewPermissions(permissions services.Permissions, log *logger.Logger) proto. } } +//nolint:dupl func (p *permissionsService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListPermissionsResponse, error) { if err := protovalidate.Validate(req); err != nil { return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) @@ -152,6 +153,7 @@ func (p *permissionsService) Update(ctx context.Context, req *proto.UpdatePermis }, nil } +//nolint:dupl func (p *permissionsService) Delete(ctx context.Context, req *proto.DeletePermissionRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) diff --git a/internal/app/rpcs/services/roles.go b/internal/app/rpcs/services/roles.go index a14ed9a..c54b54e 100644 --- a/internal/app/rpcs/services/roles.go +++ b/internal/app/rpcs/services/roles.go @@ -29,6 +29,7 @@ func NewRoles(roles services.Roles, log *logger.Logger) proto.RoleServiceServer } } +//nolint:dupl func (p *rolesService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListRolesResponse, error) { if err := protovalidate.Validate(req); err != nil { return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) @@ -175,6 +176,7 @@ func (p *rolesService) Update(ctx context.Context, req *proto.UpdateRoleRequest) }, nil } +//nolint:dupl func (p *rolesService) Delete(ctx context.Context, req *proto.DeleteRoleRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) diff --git a/internal/app/rpcs/services/scopes.go b/internal/app/rpcs/services/scopes.go index 22cd839..f58ab40 100644 --- a/internal/app/rpcs/services/scopes.go +++ b/internal/app/rpcs/services/scopes.go @@ -29,6 +29,7 @@ func NewScopes(scopes services.Scopes, log *logger.Logger) proto.ScopeServiceSer } } +//nolint:dupl func (p *scopesService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListScopesResponse, error) { if err := protovalidate.Validate(req); err != nil { return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) @@ -152,6 +153,7 @@ func (p *scopesService) Update(ctx context.Context, req *proto.UpdateScopeReques }, nil } +//nolint:dupl func (p *scopesService) Delete(ctx context.Context, req *proto.DeleteScopeRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) diff --git a/internal/app/rpcs/services/tokens.go b/internal/app/rpcs/services/tokens.go new file mode 100644 index 0000000..a0bc4b8 --- /dev/null +++ b/internal/app/rpcs/services/tokens.go @@ -0,0 +1,95 @@ +package services + +import ( + "context" + + "github.com/bufbuild/protovalidate-go" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "loki/internal/app/errors" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +type tokensService struct { + proto.UnimplementedTokenServiceServer + tokens services.Tokens + log *logger.Logger +} + +func NewTokens(tokens services.Tokens, log *logger.Logger) proto.TokenServiceServer { + return &tokensService{ + tokens: tokens, + log: log, + } +} + +//nolint:dupl +func (p *tokensService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListTokensResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + } + + pagination := &services.Pagination{ + Page: req.Limit, + PerPage: req.Offset, + } + + rows, total, err := p.tokens.List(ctx, pagination) + if err != nil { + p.log.Error().Err(err).Msg("Failed to fetch tokens") + return nil, status.Error(codes.Internal, "failed to fetch tokens") + } + + collection := make([]*proto.Token, 0, len(rows)) + for _, row := range rows { + collection = append(collection, &proto.Token{ + Id: row.ID.String(), + UserId: row.UserId.String(), + Type: row.Type, + Value: row.Value, + ExpiresAt: timestamppb.New(row.ExpiresAt), + }) + } + + return &proto.ListTokensResponse{ + Data: collection, + Meta: &proto.PaginationMeta{ + Page: pagination.Page, + Per: pagination.PerPage, + Total: total, + }, + }, nil +} + +//nolint:dupl +func (p *tokensService) Delete(ctx context.Context, req *proto.DeleteTokenRequest) (*emptypb.Empty, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse token ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + _, err = p.tokens.Delete(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete token") + + switch { + case errors.Is(err, errors.ErrTokenNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to delete token") + } + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/app/rpcs/services/tokens_test.go b/internal/app/rpcs/services/tokens_test.go new file mode 100644 index 0000000..e8425d9 --- /dev/null +++ b/internal/app/rpcs/services/tokens_test.go @@ -0,0 +1,204 @@ +package services + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +func Test_Tokens_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + tokens := services.NewMockTokens(ctrl) + log := logger.NewLogger() + service := NewTokens(tokens, log) + + tests := []struct { + name string + before func() + request *proto.PaginatedListRequest + expected *proto.ListTokensResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + tokens.EXPECT().List(ctx, gomock.Any()).Return([]models.Token{ + { + ID: uuid.MustParse("10000000-1000-1000-6000-000000000001"), + UserId: uuid.MustParse("10000000-1000-1000-1234-000000000001"), + Type: models.AccessTokenType, + Value: "access-token-value", + }, + { + ID: uuid.MustParse("10000000-1000-1000-6000-000000000002"), + UserId: uuid.MustParse("10000000-1000-1000-1234-000000000002"), + Type: models.RefreshTokenType, + Value: "refresh-token-value", + }, + }, uint64(2), nil) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: &proto.ListTokensResponse{ + Data: []*proto.Token{ + { + Id: "10000000-1000-1000-6000-000000000001", + UserId: "10000000-1000-1000-1234-000000000001", + Type: "access_token", + Value: "access-token-value", + }, + { + Id: "10000000-1000-1000-6000-000000000002", + UserId: "10000000-1000-1000-1234-000000000002", + Type: "refresh_token", + Value: "refresh-token-value", + }, + }, + Meta: &proto.PaginationMeta{ + Page: 1, + Per: 10, + Total: 2, + }, + }, + error: false, + }, + { + name: "Error", + before: func() { + tokens.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, + code: codes.Internal, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.List(ctx, tt.request) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.expected.Data), len(result.Data)) + assert.Equal(t, tt.expected.Meta.Total, result.Meta.Total) + for i, token := range tt.expected.Data { + assert.Equal(t, token.Id, result.Data[i].Id) + assert.Equal(t, token.UserId, result.Data[i].UserId) + assert.Equal(t, token.Type, result.Data[i].Type) + assert.Equal(t, token.Value, result.Data[i].Value) + } + } + }) + } +} + +func Test_Tokens_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + tokens := services.NewMockTokens(ctrl) + log := logger.NewLogger() + service := NewTokens(tokens, log) + + id := uuid.MustParse("10000000-1000-1000-6000-000000000001") + + tests := []struct { + name string + before func() + req *proto.DeleteTokenRequest + expected *emptypb.Empty + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + tokens.EXPECT().Delete(ctx, id).Return(true, nil) + }, + req: &proto.DeleteTokenRequest{ + Id: id.String(), + }, + expected: &emptypb.Empty{}, + error: false, + }, + { + name: "Not Found", + before: func() { + tokens.EXPECT().Delete(ctx, id).Return(false, errors.ErrTokenNotFound) + }, + req: &proto.DeleteTokenRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + tokens.EXPECT().Delete(ctx, id).Return(false, assert.AnError) + }, + req: &proto.DeleteTokenRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.DeleteTokenRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Delete(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} From 400e652470f0a70e21bbbc04947d9907ba5c1fc0 Mon Sep 17 00:00:00 2001 From: tab Date: Wed, 2 Apr 2025 15:51:47 +0300 Subject: [PATCH 06/20] feat(grpc) Add gRPC user service Added user service with CRUD operations for managing users --- internal/app/rpcs/proto/sso/v1/user.pb.go | 685 ++++++++++++++++++ .../app/rpcs/proto/sso/v1/user_grpc.pb.go | 278 +++++++ internal/app/rpcs/registry.go | 4 + internal/app/rpcs/registry_test.go | 9 + internal/app/rpcs/services/module.go | 1 + internal/app/rpcs/services/users.go | 231 ++++++ internal/app/rpcs/services/users_test.go | 509 +++++++++++++ 7 files changed, 1717 insertions(+) create mode 100644 internal/app/rpcs/proto/sso/v1/user.pb.go create mode 100644 internal/app/rpcs/proto/sso/v1/user_grpc.pb.go create mode 100644 internal/app/rpcs/services/users.go create mode 100644 internal/app/rpcs/services/users_test.go diff --git a/internal/app/rpcs/proto/sso/v1/user.pb.go b/internal/app/rpcs/proto/sso/v1/user.pb.go new file mode 100644 index 0000000..64bf80e --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/user.pb.go @@ -0,0 +1,685 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: sso/v1/user.proto + +package ssov1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// User represents a user object +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + IdentityNumber string `protobuf:"bytes,2,opt,name=identity_number,json=identityNumber,proto3" json:"identity_number,omitempty"` + PersonalCode string `protobuf:"bytes,3,opt,name=personal_code,json=personalCode,proto3" json:"personal_code,omitempty"` + FirstName string `protobuf:"bytes,4,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` + LastName string `protobuf:"bytes,5,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + RoleIds []string `protobuf:"bytes,6,rep,name=role_ids,json=roleIds,proto3" json:"role_ids,omitempty"` + ScopeIds []string `protobuf:"bytes,7,rep,name=scope_ids,json=scopeIds,proto3" json:"scope_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_sso_v1_user_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *User) GetIdentityNumber() string { + if x != nil { + return x.IdentityNumber + } + return "" +} + +func (x *User) GetPersonalCode() string { + if x != nil { + return x.PersonalCode + } + return "" +} + +func (x *User) GetFirstName() string { + if x != nil { + return x.FirstName + } + return "" +} + +func (x *User) GetLastName() string { + if x != nil { + return x.LastName + } + return "" +} + +func (x *User) GetRoleIds() []string { + if x != nil { + return x.RoleIds + } + return nil +} + +func (x *User) GetScopeIds() []string { + if x != nil { + return x.ScopeIds + } + return nil +} + +// ListUsersResponse is the response for the List method +type ListUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data []*User `protobuf:"bytes,1,rep,name=data,proto3" json:"data,omitempty"` + Meta *PaginationMeta `protobuf:"bytes,2,opt,name=meta,proto3" json:"meta,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersResponse) Reset() { + *x = ListUsersResponse{} + mi := &file_sso_v1_user_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersResponse) ProtoMessage() {} + +func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. +func (*ListUsersResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{1} +} + +func (x *ListUsersResponse) GetData() []*User { + if x != nil { + return x.Data + } + return nil +} + +func (x *ListUsersResponse) GetMeta() *PaginationMeta { + if x != nil { + return x.Meta + } + return nil +} + +// GetUserRequest is the request for the Get method +type GetUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserRequest) Reset() { + *x = GetUserRequest{} + mi := &file_sso_v1_user_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserRequest) ProtoMessage() {} + +func (x *GetUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead. +func (*GetUserRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{2} +} + +func (x *GetUserRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +// GetUserResponse is the response for the Get method +type GetUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *User `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserResponse) Reset() { + *x = GetUserResponse{} + mi := &file_sso_v1_user_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserResponse) ProtoMessage() {} + +func (x *GetUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead. +func (*GetUserResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{3} +} + +func (x *GetUserResponse) GetData() *User { + if x != nil { + return x.Data + } + return nil +} + +// CreateUserRequest is the request for the Create method +type CreateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + IdentityNumber string `protobuf:"bytes,1,opt,name=identity_number,json=identityNumber,proto3" json:"identity_number,omitempty"` + PersonalCode string `protobuf:"bytes,2,opt,name=personal_code,json=personalCode,proto3" json:"personal_code,omitempty"` + FirstName string `protobuf:"bytes,3,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` + LastName string `protobuf:"bytes,4,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + mi := &file_sso_v1_user_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateUserRequest) GetIdentityNumber() string { + if x != nil { + return x.IdentityNumber + } + return "" +} + +func (x *CreateUserRequest) GetPersonalCode() string { + if x != nil { + return x.PersonalCode + } + return "" +} + +func (x *CreateUserRequest) GetFirstName() string { + if x != nil { + return x.FirstName + } + return "" +} + +func (x *CreateUserRequest) GetLastName() string { + if x != nil { + return x.LastName + } + return "" +} + +// CreateUserResponse is the response for the Create method +type CreateUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *User `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserResponse) Reset() { + *x = CreateUserResponse{} + mi := &file_sso_v1_user_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserResponse) ProtoMessage() {} + +func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead. +func (*CreateUserResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{5} +} + +func (x *CreateUserResponse) GetData() *User { + if x != nil { + return x.Data + } + return nil +} + +// UpdateUserRequest is the request for the Update method +type UpdateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + IdentityNumber string `protobuf:"bytes,2,opt,name=identity_number,json=identityNumber,proto3" json:"identity_number,omitempty"` + PersonalCode string `protobuf:"bytes,3,opt,name=personal_code,json=personalCode,proto3" json:"personal_code,omitempty"` + FirstName string `protobuf:"bytes,4,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` + LastName string `protobuf:"bytes,5,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + RoleIds []string `protobuf:"bytes,6,rep,name=role_ids,json=roleIds,proto3" json:"role_ids,omitempty"` + ScopeIds []string `protobuf:"bytes,7,rep,name=scope_ids,json=scopeIds,proto3" json:"scope_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} + mi := &file_sso_v1_user_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserRequest) ProtoMessage() {} + +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateUserRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateUserRequest) GetIdentityNumber() string { + if x != nil { + return x.IdentityNumber + } + return "" +} + +func (x *UpdateUserRequest) GetPersonalCode() string { + if x != nil { + return x.PersonalCode + } + return "" +} + +func (x *UpdateUserRequest) GetFirstName() string { + if x != nil { + return x.FirstName + } + return "" +} + +func (x *UpdateUserRequest) GetLastName() string { + if x != nil { + return x.LastName + } + return "" +} + +func (x *UpdateUserRequest) GetRoleIds() []string { + if x != nil { + return x.RoleIds + } + return nil +} + +func (x *UpdateUserRequest) GetScopeIds() []string { + if x != nil { + return x.ScopeIds + } + return nil +} + +// UpdateUserResponse is the response for the Update method +type UpdateUserResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Data *User `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserResponse) Reset() { + *x = UpdateUserResponse{} + mi := &file_sso_v1_user_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserResponse) ProtoMessage() {} + +func (x *UpdateUserResponse) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserResponse.ProtoReflect.Descriptor instead. +func (*UpdateUserResponse) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateUserResponse) GetData() *User { + if x != nil { + return x.Data + } + return nil +} + +// DeleteUserRequest is the request for the Delete method +type DeleteUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserRequest) Reset() { + *x = DeleteUserRequest{} + mi := &file_sso_v1_user_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserRequest) ProtoMessage() {} + +func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_sso_v1_user_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead. +func (*DeleteUserRequest) Descriptor() ([]byte, []int) { + return file_sso_v1_user_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteUserRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +var File_sso_v1_user_proto protoreflect.FileDescriptor + +const file_sso_v1_user_proto_rawDesc = "" + + "\n" + + "\x11sso/v1/user.proto\x12\x06sso.v1\x1a\x1bbuf/validate/validate.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x17sso/v1/pagination.proto\"\xac\x02\n" + + "\x04User\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x122\n" + + "\x0fidentity_number\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x0f\x18\x14R\x0eidentityNumber\x12.\n" + + "\rpersonal_code\x18\x03 \x01(\tB\t\xbaH\x06r\x04\x10\v\x18\x14R\fpersonalCode\x12(\n" + + "\n" + + "first_name\x18\x04 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\tfirstName\x12&\n" + + "\tlast_name\x18\x05 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\blastName\x12(\n" + + "\brole_ids\x18\x06 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\aroleIds\x12*\n" + + "\tscope_ids\x18\a \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\bscopeIds\"a\n" + + "\x11ListUsersResponse\x12 \n" + + "\x04data\x18\x01 \x03(\v2\f.sso.v1.UserR\x04data\x12*\n" + + "\x04meta\x18\x02 \x01(\v2\x16.sso.v1.PaginationMetaR\x04meta\"*\n" + + "\x0eGetUserRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"3\n" + + "\x0fGetUserResponse\x12 \n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.UserR\x04data\"\xc9\x01\n" + + "\x11CreateUserRequest\x122\n" + + "\x0fidentity_number\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x0f\x18\x14R\x0eidentityNumber\x12.\n" + + "\rpersonal_code\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\v\x18\x14R\fpersonalCode\x12(\n" + + "\n" + + "first_name\x18\x03 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\tfirstName\x12&\n" + + "\tlast_name\x18\x04 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\blastName\"6\n" + + "\x12CreateUserResponse\x12 \n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.UserR\x04data\"\xb9\x02\n" + + "\x11UpdateUserRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\x122\n" + + "\x0fidentity_number\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\x0f\x18\x14R\x0eidentityNumber\x12.\n" + + "\rpersonal_code\x18\x03 \x01(\tB\t\xbaH\x06r\x04\x10\v\x18\x14R\fpersonalCode\x12(\n" + + "\n" + + "first_name\x18\x04 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\tfirstName\x12&\n" + + "\tlast_name\x18\x05 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\blastName\x12(\n" + + "\brole_ids\x18\x06 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\aroleIds\x12*\n" + + "\tscope_ids\x18\a \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\bscopeIds\"6\n" + + "\x12UpdateUserResponse\x12 \n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.UserR\x04data\"-\n" + + "\x11DeleteUserRequest\x12\x18\n" + + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id2\xcf\x02\n" + + "\vUserService\x12A\n" + + "\x04List\x12\x1c.sso.v1.PaginatedListRequest\x1a\x19.sso.v1.ListUsersResponse\"\x00\x128\n" + + "\x03Get\x12\x16.sso.v1.GetUserRequest\x1a\x17.sso.v1.GetUserResponse\"\x00\x12A\n" + + "\x06Create\x12\x19.sso.v1.CreateUserRequest\x1a\x1a.sso.v1.CreateUserResponse\"\x00\x12A\n" + + "\x06Update\x12\x19.sso.v1.UpdateUserRequest\x1a\x1a.sso.v1.UpdateUserResponse\"\x00\x12=\n" + + "\x06Delete\x12\x19.sso.v1.DeleteUserRequest\x1a\x16.google.protobuf.Empty\"\x00B+Z)loki/internal/app/rpcs/proto/sso/v1;ssov1b\x06proto3" + +var ( + file_sso_v1_user_proto_rawDescOnce sync.Once + file_sso_v1_user_proto_rawDescData []byte +) + +func file_sso_v1_user_proto_rawDescGZIP() []byte { + file_sso_v1_user_proto_rawDescOnce.Do(func() { + file_sso_v1_user_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_sso_v1_user_proto_rawDesc), len(file_sso_v1_user_proto_rawDesc))) + }) + return file_sso_v1_user_proto_rawDescData +} + +var file_sso_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_sso_v1_user_proto_goTypes = []any{ + (*User)(nil), // 0: sso.v1.User + (*ListUsersResponse)(nil), // 1: sso.v1.ListUsersResponse + (*GetUserRequest)(nil), // 2: sso.v1.GetUserRequest + (*GetUserResponse)(nil), // 3: sso.v1.GetUserResponse + (*CreateUserRequest)(nil), // 4: sso.v1.CreateUserRequest + (*CreateUserResponse)(nil), // 5: sso.v1.CreateUserResponse + (*UpdateUserRequest)(nil), // 6: sso.v1.UpdateUserRequest + (*UpdateUserResponse)(nil), // 7: sso.v1.UpdateUserResponse + (*DeleteUserRequest)(nil), // 8: sso.v1.DeleteUserRequest + (*PaginationMeta)(nil), // 9: sso.v1.PaginationMeta + (*PaginatedListRequest)(nil), // 10: sso.v1.PaginatedListRequest + (*emptypb.Empty)(nil), // 11: google.protobuf.Empty +} +var file_sso_v1_user_proto_depIdxs = []int32{ + 0, // 0: sso.v1.ListUsersResponse.data:type_name -> sso.v1.User + 9, // 1: sso.v1.ListUsersResponse.meta:type_name -> sso.v1.PaginationMeta + 0, // 2: sso.v1.GetUserResponse.data:type_name -> sso.v1.User + 0, // 3: sso.v1.CreateUserResponse.data:type_name -> sso.v1.User + 0, // 4: sso.v1.UpdateUserResponse.data:type_name -> sso.v1.User + 10, // 5: sso.v1.UserService.List:input_type -> sso.v1.PaginatedListRequest + 2, // 6: sso.v1.UserService.Get:input_type -> sso.v1.GetUserRequest + 4, // 7: sso.v1.UserService.Create:input_type -> sso.v1.CreateUserRequest + 6, // 8: sso.v1.UserService.Update:input_type -> sso.v1.UpdateUserRequest + 8, // 9: sso.v1.UserService.Delete:input_type -> sso.v1.DeleteUserRequest + 1, // 10: sso.v1.UserService.List:output_type -> sso.v1.ListUsersResponse + 3, // 11: sso.v1.UserService.Get:output_type -> sso.v1.GetUserResponse + 5, // 12: sso.v1.UserService.Create:output_type -> sso.v1.CreateUserResponse + 7, // 13: sso.v1.UserService.Update:output_type -> sso.v1.UpdateUserResponse + 11, // 14: sso.v1.UserService.Delete:output_type -> google.protobuf.Empty + 10, // [10:15] is the sub-list for method output_type + 5, // [5:10] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_sso_v1_user_proto_init() } +func file_sso_v1_user_proto_init() { + if File_sso_v1_user_proto != nil { + return + } + file_sso_v1_pagination_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_sso_v1_user_proto_rawDesc), len(file_sso_v1_user_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_sso_v1_user_proto_goTypes, + DependencyIndexes: file_sso_v1_user_proto_depIdxs, + MessageInfos: file_sso_v1_user_proto_msgTypes, + }.Build() + File_sso_v1_user_proto = out.File + file_sso_v1_user_proto_goTypes = nil + file_sso_v1_user_proto_depIdxs = nil +} diff --git a/internal/app/rpcs/proto/sso/v1/user_grpc.pb.go b/internal/app/rpcs/proto/sso/v1/user_grpc.pb.go new file mode 100644 index 0000000..9ac9198 --- /dev/null +++ b/internal/app/rpcs/proto/sso/v1/user_grpc.pb.go @@ -0,0 +1,278 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: sso/v1/user.proto + +package ssov1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + UserService_List_FullMethodName = "/sso.v1.UserService/List" + UserService_Get_FullMethodName = "/sso.v1.UserService/Get" + UserService_Create_FullMethodName = "/sso.v1.UserService/Create" + UserService_Update_FullMethodName = "/sso.v1.UserService/Update" + UserService_Delete_FullMethodName = "/sso.v1.UserService/Delete" +) + +// UserServiceClient is the client API for UserService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// User service provides CRUD operations for managing users +type UserServiceClient interface { + List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) + Get(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) + Create(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) + Update(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) + Delete(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type userServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { + return &userServiceClient{cc} +} + +func (c *userServiceClient) List(ctx context.Context, in *PaginatedListRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUsersResponse) + err := c.cc.Invoke(ctx, UserService_List_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) Get(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetUserResponse) + err := c.cc.Invoke(ctx, UserService_Get_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) Create(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateUserResponse) + err := c.cc.Invoke(ctx, UserService_Create_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) Update(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*UpdateUserResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateUserResponse) + err := c.cc.Invoke(ctx, UserService_Update_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) Delete(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, UserService_Delete_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UserServiceServer is the server API for UserService service. +// All implementations must embed UnimplementedUserServiceServer +// for forward compatibility. +// +// User service provides CRUD operations for managing users +type UserServiceServer interface { + List(context.Context, *PaginatedListRequest) (*ListUsersResponse, error) + Get(context.Context, *GetUserRequest) (*GetUserResponse, error) + Create(context.Context, *CreateUserRequest) (*CreateUserResponse, error) + Update(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) + Delete(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedUserServiceServer() +} + +// UnimplementedUserServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedUserServiceServer struct{} + +func (UnimplementedUserServiceServer) List(context.Context, *PaginatedListRequest) (*ListUsersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method List not implemented") +} +func (UnimplementedUserServiceServer) Get(context.Context, *GetUserRequest) (*GetUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") +} +func (UnimplementedUserServiceServer) Create(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Create not implemented") +} +func (UnimplementedUserServiceServer) Update(context.Context, *UpdateUserRequest) (*UpdateUserResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") +} +func (UnimplementedUserServiceServer) Delete(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented") +} +func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} +func (UnimplementedUserServiceServer) testEmbeddedByValue() {} + +// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UserServiceServer will +// result in compilation errors. +type UnsafeUserServiceServer interface { + mustEmbedUnimplementedUserServiceServer() +} + +func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { + // If the following call pancis, it indicates UnimplementedUserServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&UserService_ServiceDesc, srv) +} + +func _UserService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PaginatedListRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).List(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_List_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).List(ctx, req.(*PaginatedListRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).Get(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_Get_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).Get(ctx, req.(*GetUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).Create(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_Create_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).Create(ctx, req.(*CreateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).Update(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_Update_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).Update(ctx, req.(*UpdateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).Delete(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_Delete_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).Delete(ctx, req.(*DeleteUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UserService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "sso.v1.UserService", + HandlerType: (*UserServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "List", + Handler: _UserService_List_Handler, + }, + { + MethodName: "Get", + Handler: _UserService_Get_Handler, + }, + { + MethodName: "Create", + Handler: _UserService_Create_Handler, + }, + { + MethodName: "Update", + Handler: _UserService_Update_Handler, + }, + { + MethodName: "Delete", + Handler: _UserService_Delete_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "sso/v1/user.proto", +} diff --git a/internal/app/rpcs/registry.go b/internal/app/rpcs/registry.go index 88f86c1..d101e09 100644 --- a/internal/app/rpcs/registry.go +++ b/internal/app/rpcs/registry.go @@ -11,6 +11,7 @@ type Registry struct { roles proto.RoleServiceServer scopes proto.ScopeServiceServer tokens proto.TokenServiceServer + users proto.UserServiceServer } func NewRegistry( @@ -18,12 +19,14 @@ func NewRegistry( roles proto.RoleServiceServer, scopes proto.ScopeServiceServer, tokens proto.TokenServiceServer, + users proto.UserServiceServer, ) *Registry { return &Registry{ permissions: permissions, roles: roles, scopes: scopes, tokens: tokens, + users: users, } } @@ -32,4 +35,5 @@ func (r *Registry) RegisterAll(server *grpc.Server) { proto.RegisterRoleServiceServer(server, r.roles) proto.RegisterScopeServiceServer(server, r.scopes) proto.RegisterTokenServiceServer(server, r.tokens) + proto.RegisterUserServiceServer(server, r.users) } diff --git a/internal/app/rpcs/registry_test.go b/internal/app/rpcs/registry_test.go index 5afb0dc..8aca183 100644 --- a/internal/app/rpcs/registry_test.go +++ b/internal/app/rpcs/registry_test.go @@ -25,12 +25,17 @@ type tokenService struct { proto.UnimplementedTokenServiceServer } +type userService struct { + proto.UnimplementedUserServiceServer +} + func Test_Registry_RegisterAll(t *testing.T) { registry := NewRegistry( &permissionService{}, &roleService{}, &scopeService{}, &tokenService{}, + &userService{}, ) assert.NotNil(t, registry) @@ -39,4 +44,8 @@ func Test_Registry_RegisterAll(t *testing.T) { serviceInfo := server.GetServiceInfo() assert.Contains(t, serviceInfo, "sso.v1.PermissionService") + assert.Contains(t, serviceInfo, "sso.v1.RoleService") + assert.Contains(t, serviceInfo, "sso.v1.ScopeService") + assert.Contains(t, serviceInfo, "sso.v1.TokenService") + assert.Contains(t, serviceInfo, "sso.v1.UserService") } diff --git a/internal/app/rpcs/services/module.go b/internal/app/rpcs/services/module.go index 4ce9ead..5569951 100644 --- a/internal/app/rpcs/services/module.go +++ b/internal/app/rpcs/services/module.go @@ -7,4 +7,5 @@ var Module = fx.Options( fx.Provide(NewRoles), fx.Provide(NewScopes), fx.Provide(NewTokens), + fx.Provide(NewUsers), ) diff --git a/internal/app/rpcs/services/users.go b/internal/app/rpcs/services/users.go new file mode 100644 index 0000000..809d761 --- /dev/null +++ b/internal/app/rpcs/services/users.go @@ -0,0 +1,231 @@ +package services + +import ( + "context" + "fmt" + + "github.com/bufbuild/protovalidate-go" + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +type usersService struct { + proto.UnimplementedUserServiceServer + users services.Users + log *logger.Logger +} + +func NewUsers(users services.Users, log *logger.Logger) proto.UserServiceServer { + return &usersService{ + users: users, + log: log, + } +} + +//nolint:dupl +func (p *usersService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListUsersResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + } + + pagination := &services.Pagination{ + Page: req.Limit, + PerPage: req.Offset, + } + + rows, total, err := p.users.List(ctx, pagination) + if err != nil { + p.log.Error().Err(err).Msg("Failed to fetch users") + return nil, status.Error(codes.Internal, "failed to fetch users") + } + + collection := make([]*proto.User, 0, len(rows)) + for _, row := range rows { + collection = append(collection, &proto.User{ + Id: row.ID.String(), + IdentityNumber: row.IdentityNumber, + PersonalCode: row.PersonalCode, + FirstName: row.FirstName, + LastName: row.LastName, + }) + } + + return &proto.ListUsersResponse{ + Data: collection, + Meta: &proto.PaginationMeta{ + Page: pagination.Page, + Per: pagination.PerPage, + Total: total, + }, + }, nil +} + +func (p *usersService) Get(ctx context.Context, req *proto.GetUserRequest) (*proto.GetUserResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse user ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + user, err := p.users.FindById(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get user") + if errors.Is(err, errors.ErrUserNotFound) { + return nil, status.Error(codes.NotFound, "user not found") + } + return nil, status.Error(codes.Internal, "failed to get user") + } + + roleIds := make([]string, 0, len(user.RoleIDs)) + for _, roleId := range user.RoleIDs { + roleIds = append(roleIds, roleId.String()) + } + + scopeIds := make([]string, 0, len(user.ScopeIDs)) + for _, scopeId := range user.ScopeIDs { + scopeIds = append(scopeIds, scopeId.String()) + } + + return &proto.GetUserResponse{ + Data: &proto.User{ + Id: user.ID.String(), + IdentityNumber: user.IdentityNumber, + PersonalCode: user.PersonalCode, + FirstName: user.FirstName, + LastName: user.LastName, + RoleIds: roleIds, + ScopeIds: scopeIds, + }, + }, nil +} + +func (p *usersService) Create(ctx context.Context, req *proto.CreateUserRequest) (*proto.CreateUserResponse, error) { + fmt.Println("--- create ---") + fmt.Println(req) + + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + user, err := p.users.Create(ctx, &models.User{ + IdentityNumber: req.IdentityNumber, + PersonalCode: req.PersonalCode, + FirstName: req.FirstName, + LastName: req.LastName, + }) + if err != nil { + p.log.Error().Err(err).Str("identity_number", req.IdentityNumber).Msg("Failed to create user") + return nil, status.Error(codes.Internal, err.Error()) + } + + return &proto.CreateUserResponse{ + Data: &proto.User{ + Id: user.ID.String(), + IdentityNumber: user.IdentityNumber, + PersonalCode: user.PersonalCode, + FirstName: user.FirstName, + LastName: user.LastName, + }, + }, nil +} + +func (p *usersService) Update(ctx context.Context, req *proto.UpdateUserRequest) (*proto.UpdateUserResponse, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Invalid UUID format") + return nil, status.Error(codes.InvalidArgument, "invalid user id format") + } + + roleIds := make([]uuid.UUID, 0, len(req.RoleIds)) + for _, roleId := range req.RoleIds { + id, err := uuid.Parse(roleId) + if err != nil { + p.log.Error().Err(err).Str("id", roleId).Msg("Invalid UUID format") + return nil, status.Error(codes.InvalidArgument, "invalid role id format") + } + roleIds = append(roleIds, id) + } + + scopeIds := make([]uuid.UUID, 0, len(req.ScopeIds)) + for _, scopeId := range req.ScopeIds { + id, err := uuid.Parse(scopeId) + if err != nil { + p.log.Error().Err(err).Str("id", scopeId).Msg("Invalid UUID format") + return nil, status.Error(codes.InvalidArgument, "invalid scope id format") + } + scopeIds = append(scopeIds, id) + } + + user, err := p.users.Update(ctx, &models.User{ + ID: id, + IdentityNumber: req.IdentityNumber, + PersonalCode: req.PersonalCode, + FirstName: req.FirstName, + LastName: req.LastName, + RoleIDs: roleIds, + ScopeIDs: scopeIds, + }) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update user") + + switch { + case errors.Is(err, errors.ErrUserNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to update user") + } + } + + return &proto.UpdateUserResponse{ + Data: &proto.User{ + Id: user.ID.String(), + IdentityNumber: user.IdentityNumber, + PersonalCode: user.PersonalCode, + FirstName: user.FirstName, + LastName: user.LastName, + }, + }, nil +} + +//nolint:dupl +func (p *usersService) Delete(ctx context.Context, req *proto.DeleteUserRequest) (*emptypb.Empty, error) { + if err := protovalidate.Validate(req); err != nil { + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + } + + id, err := uuid.Parse(req.Id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to parse user ID") + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + + _, err = p.users.Delete(ctx, id) + if err != nil { + p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete user") + + switch { + case errors.Is(err, errors.ErrUserNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to delete user") + } + } + + return &emptypb.Empty{}, nil +} diff --git a/internal/app/rpcs/services/users_test.go b/internal/app/rpcs/services/users_test.go new file mode 100644 index 0000000..3f90410 --- /dev/null +++ b/internal/app/rpcs/services/users_test.go @@ -0,0 +1,509 @@ +package services + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "loki/internal/app/errors" + "loki/internal/app/models" + proto "loki/internal/app/rpcs/proto/sso/v1" + "loki/internal/app/services" + "loki/pkg/logger" +) + +func Test_Users_List(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + users := services.NewMockUsers(ctrl) + log := logger.NewLogger() + service := NewUsers(users, log) + + tests := []struct { + name string + before func() + request *proto.PaginatedListRequest + expected *proto.ListUsersResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + users.EXPECT().List(ctx, gomock.Any()).Return([]models.User{ + { + ID: uuid.MustParse("10000000-1000-1000-1234-000000000001"), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + { + ID: uuid.MustParse("10000000-1000-1000-1234-000000000002"), + IdentityNumber: "PNOEE-987654321", + PersonalCode: "987654321", + FirstName: "Jane", + LastName: "TESTNUMBER", + }, + }, uint64(2), nil) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: &proto.ListUsersResponse{ + Data: []*proto.User{ + { + Id: "10000000-1000-1000-1234-000000000001", + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + { + Id: "10000000-1000-1000-1234-000000000002", + IdentityNumber: "PNOEE-987654321", + PersonalCode: "987654321", + FirstName: "Jane", + LastName: "TESTNUMBER", + }, + }, + Meta: &proto.PaginationMeta{ + Page: 1, + Per: 10, + Total: 2, + }, + }, + error: false, + }, + { + name: "Error", + before: func() { + users.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, + code: codes.Internal, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.List(ctx, tt.request) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.expected.Data), len(result.Data)) + assert.Equal(t, tt.expected.Meta.Total, result.Meta.Total) + for i, user := range tt.expected.Data { + assert.Equal(t, user.Id, result.Data[i].Id) + assert.Equal(t, user.IdentityNumber, result.Data[i].IdentityNumber) + assert.Equal(t, user.PersonalCode, result.Data[i].PersonalCode) + assert.Equal(t, user.FirstName, result.Data[i].FirstName) + assert.Equal(t, user.LastName, result.Data[i].LastName) + } + } + }) + } +} + +func Test_Users_Get(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + users := services.NewMockUsers(ctrl) + log := logger.NewLogger() + service := NewUsers(users, log) + + id := uuid.MustParse("10000000-1000-1000-1234-000000000001") + + tests := []struct { + name string + before func() + req *proto.GetUserRequest + expected *proto.GetUserResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + users.EXPECT().FindById(ctx, id).Return(&models.User{ + ID: id, + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, nil) + }, + req: &proto.GetUserRequest{ + Id: id.String(), + }, + expected: &proto.GetUserResponse{ + Data: &proto.User{ + Id: id.String(), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + users.EXPECT().FindById(ctx, id).Return(nil, errors.ErrUserNotFound) + }, + req: &proto.GetUserRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.GetUserRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Get(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.IdentityNumber, result.Data.IdentityNumber) + assert.Equal(t, tt.expected.Data.PersonalCode, result.Data.PersonalCode) + assert.Equal(t, tt.expected.Data.FirstName, result.Data.FirstName) + assert.Equal(t, tt.expected.Data.LastName, result.Data.LastName) + } + }) + } +} + +func Test_Users_Create(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + users := services.NewMockUsers(ctrl) + log := logger.NewLogger() + service := NewUsers(users, log) + + id := uuid.MustParse("10000000-1000-1000-1234-000000000001") + + tests := []struct { + name string + before func() + req *proto.CreateUserRequest + expected *proto.CreateUserResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + users.EXPECT().Create(gomock.Any(), &models.User{ + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }).Return(&models.User{ + ID: id, + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, nil) + }, + req: &proto.CreateUserRequest{ + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + expected: &proto.CreateUserResponse{ + Data: &proto.User{ + Id: id.String(), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + }, + error: false, + }, + { + name: "Internal Error", + before: func() { + users.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.CreateUserRequest{ + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Create(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.IdentityNumber, result.Data.IdentityNumber) + assert.Equal(t, tt.expected.Data.PersonalCode, result.Data.PersonalCode) + assert.Equal(t, tt.expected.Data.FirstName, result.Data.FirstName) + assert.Equal(t, tt.expected.Data.LastName, result.Data.LastName) + } + }) + } +} + +func Test_Users_Update(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + users := services.NewMockUsers(ctrl) + log := logger.NewLogger() + service := NewUsers(users, log) + + id := uuid.MustParse("10000000-1000-1000-1234-000000000001") + + tests := []struct { + name string + before func() + req *proto.UpdateUserRequest + expected *proto.UpdateUserResponse + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + users.EXPECT().Update(ctx, gomock.Any()).Return(&models.User{ + ID: id, + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, nil) + }, + req: &proto.UpdateUserRequest{ + Id: id.String(), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, + expected: &proto.UpdateUserResponse{ + Data: &proto.User{ + Id: id.String(), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, + }, + error: false, + }, + { + name: "Not Found", + before: func() { + users.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrUserNotFound) + }, + req: &proto.UpdateUserRequest{ + Id: id.String(), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + users.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, + req: &proto.UpdateUserRequest{ + Id: id.String(), + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.UpdateUserRequest{ + Id: "invalid-uuid", + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Update(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected.Data.Id, result.Data.Id) + assert.Equal(t, tt.expected.Data.IdentityNumber, result.Data.IdentityNumber) + assert.Equal(t, tt.expected.Data.PersonalCode, result.Data.PersonalCode) + assert.Equal(t, tt.expected.Data.FirstName, result.Data.FirstName) + assert.Equal(t, tt.expected.Data.LastName, result.Data.LastName) + } + }) + } +} + +func Test_Users_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + users := services.NewMockUsers(ctrl) + log := logger.NewLogger() + service := NewUsers(users, log) + + id := uuid.MustParse("10000000-1000-1000-1234-000000000001") + + tests := []struct { + name string + before func() + req *proto.DeleteUserRequest + expected *emptypb.Empty + code codes.Code + error bool + }{ + { + name: "Success", + before: func() { + users.EXPECT().Delete(ctx, id).Return(true, nil) + }, + req: &proto.DeleteUserRequest{ + Id: id.String(), + }, + expected: &emptypb.Empty{}, + error: false, + }, + { + name: "Not Found", + before: func() { + users.EXPECT().Delete(ctx, id).Return(false, errors.ErrUserNotFound) + }, + req: &proto.DeleteUserRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.NotFound, + error: true, + }, + { + name: "Internal Error", + before: func() { + users.EXPECT().Delete(ctx, id).Return(false, assert.AnError) + }, + req: &proto.DeleteUserRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Invalid ID Format", + req: &proto.DeleteUserRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.before != nil { + tt.before() + } + + result, err := service.Delete(ctx, tt.req) + + if tt.error { + st, _ := status.FromError(err) + assert.Equal(t, tt.code, st.Code()) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} From eca58fb0beeb42c5c0705341c9af31a9a96a5523 Mon Sep 17 00:00:00 2001 From: tab Date: Thu, 3 Apr 2025 14:30:09 +0300 Subject: [PATCH 07/20] fix(errors): Add new error constants for record CRUD operations Added new error constants for failed record creation, updating, deletion, and not found errors --- internal/app/errors/errors.go | 12 ++ internal/app/services/permissions.go | 13 +- internal/app/services/permissions_test.go | 54 ++++++--- internal/app/services/roles.go | 16 ++- internal/app/services/roles_test.go | 60 +++++++++- internal/app/services/scopes.go | 13 +- internal/app/services/scopes_test.go | 60 +++++++++- internal/app/services/tokens.go | 10 +- internal/app/services/tokens_test.go | 137 +++++++++++++++++++++- internal/app/services/users.go | 19 ++- internal/app/services/users_test.go | 26 ++-- 11 files changed, 361 insertions(+), 59 deletions(-) diff --git a/internal/app/errors/errors.go b/internal/app/errors/errors.go index a5ec495..9115ee3 100644 --- a/internal/app/errors/errors.go +++ b/internal/app/errors/errors.go @@ -51,6 +51,18 @@ var ( // ErrFailedToFetchResults indicates that failed to fetch results ErrFailedToFetchResults = errors.New("failed to fetch results") + // ErrRecordNotFound indicates that the requested record could not be found + ErrRecordNotFound = errors.New("record not found") + + // ErrFailedToCreateRecord indicates that failed to create record + ErrFailedToCreateRecord = errors.New("failed to create record") + + // ErrFailedToUpdateRecord indicates that failed to update record + ErrFailedToUpdateRecord = errors.New("failed to update record") + + // ErrFailedToDeleteRecord indicates that failed to delete record + ErrFailedToDeleteRecord = errors.New("failed to delete record") + // ErrPermissionNotFound indicates that the requested permission could not be found ErrPermissionNotFound = errors.New("permission not found") diff --git a/internal/app/services/permissions.go b/internal/app/services/permissions.go index 71d010f..67c059c 100644 --- a/internal/app/services/permissions.go +++ b/internal/app/services/permissions.go @@ -36,6 +36,7 @@ func (p *permissions) List(ctx context.Context, pagination *Pagination) ([]model collection, total, err := p.repository.List(ctx, pagination.Limit(), pagination.Offset()) if err != nil { + p.log.Error().Err(err).Msg("Failed to fetch permissions") return nil, 0, errors.ErrFailedToFetchResults } @@ -48,7 +49,8 @@ func (p *permissions) Create(ctx context.Context, params *models.Permission) (*m Description: params.Description, }) if err != nil { - return nil, err + p.log.Error().Err(err).Msg("Failed to create permission") + return nil, errors.ErrFailedToCreateRecord } return permission, nil @@ -61,7 +63,8 @@ func (p *permissions) Update(ctx context.Context, params *models.Permission) (*m Description: params.Description, }) if err != nil { - return nil, err + p.log.Error().Err(err).Msg("Failed to update permission") + return nil, errors.ErrFailedToUpdateRecord } return permission, nil @@ -70,7 +73,8 @@ func (p *permissions) Update(ctx context.Context, params *models.Permission) (*m func (p *permissions) FindById(ctx context.Context, id uuid.UUID) (*models.Permission, error) { permission, err := p.repository.FindById(ctx, id) if err != nil { - return nil, err + p.log.Error().Err(err).Msg("Failed to find permission by ID") + return nil, errors.ErrRecordNotFound } return permission, nil @@ -79,7 +83,8 @@ func (p *permissions) FindById(ctx context.Context, id uuid.UUID) (*models.Permi func (p *permissions) Delete(ctx context.Context, id uuid.UUID) (bool, error) { ok, err := p.repository.Delete(ctx, id) if err != nil { - return false, err + p.log.Error().Err(err).Msg("Failed to delete permission") + return false, errors.ErrFailedToDeleteRecord } return ok, nil diff --git a/internal/app/services/permissions_test.go b/internal/app/services/permissions_test.go index 911749c..a3b0e61 100644 --- a/internal/app/services/permissions_test.go +++ b/internal/app/services/permissions_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "loki/internal/app/errors" "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" @@ -59,6 +60,16 @@ func Test_Permissions_List(t *testing.T) { }, }, total: uint64(2), + error: nil, + }, + { + name: "Error", + before: func() { + repository.EXPECT().List(ctx, uint64(10), uint64(0)).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + expected: nil, + total: 0, + error: errors.ErrFailedToFetchResults, }, } @@ -123,10 +134,10 @@ func Test_Permissions_Create(t *testing.T) { repository.EXPECT().Create(ctx, db.CreatePermissionParams{ Name: "read:self", Description: "Read own data", - }).Return(nil, assert.AnError) + }).Return(nil, errors.ErrFailedToCreateRecord) }, expected: nil, - error: assert.AnError, + error: errors.ErrFailedToCreateRecord, }, } @@ -191,10 +202,10 @@ func Test_Permissions_Update(t *testing.T) { ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), Name: "read:self", Description: "Read own data", - }).Return(nil, assert.AnError) + }).Return(nil, errors.ErrFailedToUpdateRecord) }, expected: nil, - error: assert.AnError, + error: errors.ErrFailedToUpdateRecord, }, } @@ -252,10 +263,10 @@ func Test_Permissions_FindById(t *testing.T) { { name: "Error", before: func() { - repository.EXPECT().FindById(ctx, uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(nil, assert.AnError) + repository.EXPECT().FindById(ctx, uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(nil, errors.ErrRecordNotFound) }, expected: nil, - error: assert.AnError, + error: errors.ErrRecordNotFound, }, } @@ -285,31 +296,36 @@ func Test_Permissions_Delete(t *testing.T) { log := logger.NewLogger() service := NewPermissions(repository, log) + id := uuid.MustParse("10000000-1000-1000-3000-000000000001") + tests := []struct { - name string - id uuid.UUID - error error + name string + before func() + expected bool + error error }{ { name: "Success", - id: uuid.MustParse("10000000-1000-1000-3000-000000000001"), + before: func() { + repository.EXPECT().Delete(ctx, id).Return(true, nil) + }, + expected: true, }, { - name: "Error", - id: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - error: assert.AnError, + name: "Error", + before: func() { + repository.EXPECT().Delete(ctx, id).Return(false, errors.ErrFailedToDeleteRecord) + }, + expected: false, + error: errors.ErrFailedToDeleteRecord, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.error != nil { - repository.EXPECT().Delete(ctx, tt.id).Return(false, assert.AnError) - } else { - repository.EXPECT().Delete(ctx, tt.id).Return(true, nil) - } + tt.before() - result, err := service.Delete(ctx, tt.id) + result, err := service.Delete(ctx, id) if tt.error != nil { assert.Error(t, err) diff --git a/internal/app/services/roles.go b/internal/app/services/roles.go index 6f7b4e3..ff3606a 100644 --- a/internal/app/services/roles.go +++ b/internal/app/services/roles.go @@ -38,6 +38,7 @@ func (r *roles) List(ctx context.Context, pagination *Pagination) ([]models.Role collection, total, err := r.repository.List(ctx, pagination.Limit(), pagination.Offset()) if err != nil { + r.log.Error().Err(err).Msg("Failed to fetch roles") return nil, 0, errors.ErrFailedToFetchResults } @@ -51,7 +52,8 @@ func (r *roles) Create(ctx context.Context, params *models.Role) (*models.Role, PermissionIDs: params.PermissionIDs, }) if err != nil { - return nil, err + r.log.Error().Err(err).Msg("Failed to create role") + return nil, errors.ErrFailedToCreateRecord } return role, nil @@ -65,7 +67,8 @@ func (r *roles) Update(ctx context.Context, params *models.Role) (*models.Role, PermissionIDs: params.PermissionIDs, }) if err != nil { - return nil, err + r.log.Error().Err(err).Msg("Failed to update role") + return nil, errors.ErrFailedToUpdateRecord } return role, nil @@ -74,7 +77,8 @@ func (r *roles) Update(ctx context.Context, params *models.Role) (*models.Role, func (r *roles) FindById(ctx context.Context, id uuid.UUID) (*models.Role, error) { role, err := r.repository.FindById(ctx, id) if err != nil { - return nil, err + r.log.Error().Err(err).Msg("Failed to find role by id") + return nil, errors.ErrRecordNotFound } return role, nil @@ -83,7 +87,8 @@ func (r *roles) FindById(ctx context.Context, id uuid.UUID) (*models.Role, error func (r *roles) Delete(ctx context.Context, id uuid.UUID) (bool, error) { ok, err := r.repository.Delete(ctx, id) if err != nil { - return false, err + r.log.Error().Err(err).Msg("Failed to delete role") + return false, errors.ErrFailedToDeleteRecord } return ok, nil @@ -92,7 +97,8 @@ func (r *roles) Delete(ctx context.Context, id uuid.UUID) (bool, error) { func (r *roles) FindRoleDetailsById(ctx context.Context, id uuid.UUID) (*models.Role, error) { role, err := r.repository.FindRoleDetailsById(ctx, id) if err != nil { - return nil, err + r.log.Error().Err(err).Msg("Failed to find role details by id") + return nil, errors.ErrRecordNotFound } return role, nil diff --git a/internal/app/services/roles_test.go b/internal/app/services/roles_test.go index 77ae2f4..d5b53c8 100644 --- a/internal/app/services/roles_test.go +++ b/internal/app/services/roles_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "loki/internal/app/errors" "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" @@ -69,6 +70,16 @@ func Test_Roles_List(t *testing.T) { }, }, total: uint64(3), + error: nil, + }, + { + name: "Error", + before: func() { + repository.EXPECT().List(ctx, uint64(10), uint64(0)).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + expected: nil, + total: 0, + error: errors.ErrFailedToFetchResults, }, } @@ -132,6 +143,21 @@ func Test_Roles_Create(t *testing.T) { Description: "Admin role", }, }, + { + name: "Error", + params: &models.Role{ + Name: models.AdminRoleType, + Description: "Admin role", + }, + before: func() { + repository.EXPECT().Create(ctx, db.CreateRoleParams{ + Name: models.AdminRoleType, + Description: "Admin role", + }).Return(nil, errors.ErrFailedToCreateRecord) + }, + expected: nil, + error: errors.ErrFailedToCreateRecord, + }, } for _, tt := range tests { @@ -185,6 +211,18 @@ func Test_Roles_Update(t *testing.T) { Description: "Admin role", }, }, + { + name: "Error", + before: func() { + repository.EXPECT().Update(ctx, db.UpdateRoleParams{ + ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), + Name: models.AdminRoleType, + Description: "Admin role", + }).Return(nil, errors.ErrFailedToUpdateRecord) + }, + expected: nil, + error: errors.ErrFailedToUpdateRecord, + }, } for _, tt := range tests { @@ -238,6 +276,14 @@ func Test_Roles_FindById(t *testing.T) { Description: "Admin role", }, }, + { + name: "Error", + before: func() { + repository.EXPECT().FindById(ctx, uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(nil, errors.ErrRecordNotFound) + }, + expected: nil, + error: errors.ErrRecordNotFound, + }, } for _, tt := range tests { @@ -266,6 +312,8 @@ func Test_Roles_Delete(t *testing.T) { log := logger.NewLogger() service := NewRoles(repository, log) + id := uuid.MustParse("10000000-1000-1000-1000-000000000001") + tests := []struct { name string before func() @@ -275,17 +323,25 @@ func Test_Roles_Delete(t *testing.T) { { name: "Success", before: func() { - repository.EXPECT().Delete(ctx, uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(true, nil) + repository.EXPECT().Delete(ctx, id).Return(true, nil) }, expected: true, }, + { + name: "Error", + before: func() { + repository.EXPECT().Delete(ctx, id).Return(false, errors.ErrFailedToDeleteRecord) + }, + expected: false, + error: errors.ErrFailedToDeleteRecord, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.before() - result, err := service.Delete(ctx, uuid.MustParse("10000000-1000-1000-1000-000000000001")) + result, err := service.Delete(ctx, id) if tt.error != nil { assert.Error(t, err) diff --git a/internal/app/services/scopes.go b/internal/app/services/scopes.go index 5883b30..ae93125 100644 --- a/internal/app/services/scopes.go +++ b/internal/app/services/scopes.go @@ -36,6 +36,7 @@ func (s *scopes) List(ctx context.Context, pagination *Pagination) ([]models.Sco collection, total, err := s.repository.List(ctx, pagination.Limit(), pagination.Offset()) if err != nil { + s.log.Error().Err(err).Msg("Failed to fetch scopes") return nil, 0, errors.ErrFailedToFetchResults } @@ -48,7 +49,8 @@ func (s *scopes) Create(ctx context.Context, params *models.Scope) (*models.Scop Description: params.Description, }) if err != nil { - return nil, err + s.log.Error().Err(err).Msg("Failed to create scope") + return nil, errors.ErrFailedToCreateRecord } return scope, nil @@ -61,7 +63,8 @@ func (s *scopes) Update(ctx context.Context, params *models.Scope) (*models.Scop Description: params.Description, }) if err != nil { - return nil, err + s.log.Error().Err(err).Msg("Failed to update scope") + return nil, errors.ErrFailedToUpdateRecord } return scope, nil @@ -70,7 +73,8 @@ func (s *scopes) Update(ctx context.Context, params *models.Scope) (*models.Scop func (s *scopes) FindById(ctx context.Context, id uuid.UUID) (*models.Scope, error) { scope, err := s.repository.FindById(ctx, id) if err != nil { - return nil, err + s.log.Error().Err(err).Msg("Failed to find scope by id") + return nil, errors.ErrRecordNotFound } return scope, nil @@ -79,7 +83,8 @@ func (s *scopes) FindById(ctx context.Context, id uuid.UUID) (*models.Scope, err func (s *scopes) Delete(ctx context.Context, id uuid.UUID) (bool, error) { ok, err := s.repository.Delete(ctx, id) if err != nil { - return false, err + s.log.Error().Err(err).Msg("Failed to delete scope") + return false, errors.ErrFailedToDeleteRecord } return ok, nil diff --git a/internal/app/services/scopes_test.go b/internal/app/services/scopes_test.go index 2e69019..939ba75 100644 --- a/internal/app/services/scopes_test.go +++ b/internal/app/services/scopes_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "loki/internal/app/errors" "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" @@ -59,6 +60,16 @@ func Test_Scopes_List(t *testing.T) { }, }, total: uint64(2), + error: nil, + }, + { + name: "Error", + before: func() { + repository.EXPECT().List(ctx, uint64(10), uint64(0)).Return(nil, uint64(0), errors.ErrFailedToFetchResults) + }, + expected: nil, + total: 0, + error: errors.ErrFailedToFetchResults, }, } @@ -122,6 +133,21 @@ func Test_Scopes_Create(t *testing.T) { Description: "SSO-service scope", }, }, + { + name: "Error", + params: &models.Scope{ + Name: models.SelfServiceType, + Description: "Self-service scope", + }, + before: func() { + repository.EXPECT().Create(ctx, db.CreateScopeParams{ + Name: models.SelfServiceType, + Description: "Self-service scope", + }).Return(nil, errors.ErrFailedToCreateRecord) + }, + expected: nil, + error: errors.ErrFailedToCreateRecord, + }, } for _, tt := range tests { @@ -175,6 +201,18 @@ func Test_Scopes_Update(t *testing.T) { Description: "Self-service scope", }, }, + { + name: "Error", + before: func() { + repository.EXPECT().Update(ctx, db.UpdateScopeParams{ + ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), + Name: models.SelfServiceType, + Description: "Self-service scope", + }).Return(nil, errors.ErrFailedToUpdateRecord) + }, + expected: nil, + error: errors.ErrFailedToUpdateRecord, + }, } for _, tt := range tests { @@ -228,6 +266,14 @@ func Test_Scopes_FindById(t *testing.T) { Description: "SSO-service scope", }, }, + { + name: "Error", + before: func() { + repository.EXPECT().FindById(ctx, uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(nil, errors.ErrRecordNotFound) + }, + expected: nil, + error: errors.ErrRecordNotFound, + }, } for _, tt := range tests { @@ -256,6 +302,8 @@ func Test_Scopes_Delete(t *testing.T) { log := logger.NewLogger() service := NewScopes(repository, log) + id := uuid.MustParse("10000000-1000-1000-2000-000000000001") + tests := []struct { name string before func() @@ -265,17 +313,25 @@ func Test_Scopes_Delete(t *testing.T) { { name: "Success", before: func() { - repository.EXPECT().Delete(ctx, uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(true, nil) + repository.EXPECT().Delete(ctx, id).Return(true, nil) }, expected: true, }, + { + name: "Error", + before: func() { + repository.EXPECT().Delete(ctx, id).Return(false, errors.ErrFailedToDeleteRecord) + }, + expected: false, + error: errors.ErrFailedToDeleteRecord, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.before() - result, err := service.Delete(ctx, uuid.MustParse("10000000-1000-1000-2000-000000000001")) + result, err := service.Delete(ctx, id) if tt.error != nil { assert.Error(t, err) diff --git a/internal/app/services/tokens.go b/internal/app/services/tokens.go index 6e41434..f257101 100644 --- a/internal/app/services/tokens.go +++ b/internal/app/services/tokens.go @@ -55,6 +55,7 @@ func (t *tokens) List(ctx context.Context, pagination *Pagination) ([]models.Tok collection, total, err := t.token.List(ctx, pagination.Limit(), pagination.Offset()) if err != nil { + t.log.Error().Err(err).Msg("Failed to fetch tokens") return nil, 0, errors.ErrFailedToFetchResults } @@ -64,7 +65,8 @@ func (t *tokens) List(ctx context.Context, pagination *Pagination) ([]models.Tok func (t *tokens) Create(ctx context.Context, userId uuid.UUID) (*models.User, error) { user, err := t.user.FindById(ctx, userId) if err != nil { - return nil, err + t.log.Error().Err(err).Msg("Failed to find user") + return nil, errors.ErrRecordNotFound } accessToken, refreshToken, err := t.generate(ctx, user) @@ -172,7 +174,8 @@ func (t *tokens) generate(ctx context.Context, user *models.User) (string, strin func (t *tokens) FindById(ctx context.Context, id uuid.UUID) (*models.Token, error) { token, err := t.token.FindById(ctx, id) if err != nil { - return nil, err + t.log.Error().Err(err).Msg("Failed to find token by id") + return nil, errors.ErrRecordNotFound } return token, nil @@ -181,7 +184,8 @@ func (t *tokens) FindById(ctx context.Context, id uuid.UUID) (*models.Token, err func (t *tokens) Delete(ctx context.Context, id uuid.UUID) (bool, error) { ok, err := t.token.Delete(ctx, id) if err != nil { - return false, err + t.log.Error().Err(err).Msg("Failed to delete token") + return false, errors.ErrFailedToDeleteRecord } return ok, nil diff --git a/internal/app/services/tokens_test.go b/internal/app/services/tokens_test.go index 8c17578..6e9714e 100644 --- a/internal/app/services/tokens_test.go +++ b/internal/app/services/tokens_test.go @@ -169,10 +169,10 @@ func Test_Tokens_Create(t *testing.T) { { name: "Failed to find user", before: func() { - userRepository.EXPECT().FindById(ctx, user.ID).Return(user, assert.AnError) + userRepository.EXPECT().FindById(ctx, user.ID).Return(user, errors.ErrRecordNotFound) }, expected: nil, - err: assert.AnError, + err: errors.ErrRecordNotFound, }, { name: "Failed to find user roles", @@ -486,3 +486,136 @@ func Test_Tokens_Update(t *testing.T) { }) } } + +func Test_Tokens_FindById(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissionRepository := repositories.NewMockPermissionRepository(ctrl) + roleRepository := repositories.NewMockRoleRepository(ctrl) + scopeRepository := repositories.NewMockScopeRepository(ctrl) + tokenRepository := repositories.NewMockTokenRepository(ctrl) + userRepository := repositories.NewMockUserRepository(ctrl) + + jwtService := jwt.NewMockJwt(ctrl) + log := logger.NewLogger() + service := NewTokens( + jwtService, + permissionRepository, + roleRepository, + scopeRepository, + tokenRepository, + userRepository, + log, + ) + + id, err := uuid.NewRandom() + assert.NoError(t, err) + + tests := []struct { + name string + before func() + expected *models.Token + error error + }{ + { + name: "Success", + before: func() { + tokenRepository.EXPECT().FindById(ctx, id).Return(&models.Token{}, nil) + }, + expected: &models.Token{}, + error: nil, + }, + { + name: "Error", + before: func() { + tokenRepository.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) + }, + expected: nil, + error: errors.ErrRecordNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.FindById(ctx, id) + + if tt.error != nil { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func Test_Tokens_Delete(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + ctx := context.Background() + permissionRepository := repositories.NewMockPermissionRepository(ctrl) + roleRepository := repositories.NewMockRoleRepository(ctrl) + scopeRepository := repositories.NewMockScopeRepository(ctrl) + tokenRepository := repositories.NewMockTokenRepository(ctrl) + userRepository := repositories.NewMockUserRepository(ctrl) + + jwtService := jwt.NewMockJwt(ctrl) + log := logger.NewLogger() + service := NewTokens( + jwtService, + permissionRepository, + roleRepository, + scopeRepository, + tokenRepository, + userRepository, + log, + ) + + id, err := uuid.NewRandom() + assert.NoError(t, err) + + tests := []struct { + name string + before func() + expected bool + error error + }{ + { + name: "Success", + before: func() { + tokenRepository.EXPECT().Delete(ctx, id).Return(true, nil) + }, + expected: true, + }, + { + name: "Error", + before: func() { + tokenRepository.EXPECT().Delete(ctx, id).Return(false, errors.ErrFailedToDeleteRecord) + }, + expected: false, + error: errors.ErrFailedToDeleteRecord, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.before() + + result, err := service.Delete(ctx, id) + + if tt.error != nil { + assert.Error(t, err) + assert.False(t, result) + } else { + assert.NoError(t, err) + assert.True(t, result) + } + }) + } +} diff --git a/internal/app/services/users.go b/internal/app/services/users.go index c396b3c..db8857a 100644 --- a/internal/app/services/users.go +++ b/internal/app/services/users.go @@ -39,6 +39,7 @@ func (u *users) List(ctx context.Context, pagination *Pagination) ([]models.User collection, total, err := u.repository.List(ctx, pagination.Limit(), pagination.Offset()) if err != nil { + u.log.Error().Err(err).Msg("Failed to fetch users") return nil, 0, errors.ErrFailedToFetchResults } @@ -53,7 +54,8 @@ func (u *users) Create(ctx context.Context, params *models.User) (*models.User, LastName: params.LastName, }) if err != nil { - return nil, err + u.log.Error().Err(err).Msg("Failed to create user") + return nil, errors.ErrFailedToCreateRecord } return user, nil @@ -70,7 +72,8 @@ func (u *users) Update(ctx context.Context, params *models.User) (*models.User, ScopeIDs: params.ScopeIDs, }) if err != nil { - return nil, err + u.log.Error().Err(err).Msg("Failed to update user") + return nil, errors.ErrFailedToUpdateRecord } return user, nil @@ -79,7 +82,8 @@ func (u *users) Update(ctx context.Context, params *models.User) (*models.User, func (u *users) FindById(ctx context.Context, id uuid.UUID) (*models.User, error) { user, err := u.repository.FindById(ctx, id) if err != nil { - return nil, err + u.log.Error().Err(err).Msg("Failed to find user by id") + return nil, errors.ErrRecordNotFound } return user, nil @@ -88,7 +92,8 @@ func (u *users) FindById(ctx context.Context, id uuid.UUID) (*models.User, error func (u *users) Delete(ctx context.Context, id uuid.UUID) (bool, error) { ok, err := u.repository.Delete(ctx, id) if err != nil { - return false, err + u.log.Error().Err(err).Msg("Failed to delete user") + return false, errors.ErrFailedToDeleteRecord } return ok, nil @@ -97,7 +102,8 @@ func (u *users) Delete(ctx context.Context, id uuid.UUID) (bool, error) { func (u *users) FindByIdentityNumber(ctx context.Context, identityNumber string) (*models.User, error) { user, err := u.repository.FindByIdentityNumber(ctx, identityNumber) if err != nil { - return nil, err + u.log.Error().Err(err).Msg("Failed to find user by identity number") + return nil, errors.ErrRecordNotFound } return user, nil @@ -106,7 +112,8 @@ func (u *users) FindByIdentityNumber(ctx context.Context, identityNumber string) func (u *users) FindUserDetailsById(ctx context.Context, id uuid.UUID) (*models.User, error) { user, err := u.repository.FindUserDetailsById(ctx, id) if err != nil { - return nil, err + u.log.Error().Err(err).Msg("Failed to find user details by id") + return nil, errors.ErrRecordNotFound } return user, nil diff --git a/internal/app/services/users_test.go b/internal/app/services/users_test.go index 6a974d9..ec2310f 100644 --- a/internal/app/services/users_test.go +++ b/internal/app/services/users_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" + "loki/internal/app/errors" "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" @@ -67,15 +68,16 @@ func Test_Users_List(t *testing.T) { }, }, total: uint64(2), + error: nil, }, { name: "Error", before: func() { - repository.EXPECT().List(ctx, uint64(10), uint64(0)).Return(nil, uint64(0), assert.AnError) + repository.EXPECT().List(ctx, uint64(10), uint64(0)).Return(nil, uint64(0), errors.ErrFailedToFetchResults) }, expected: nil, total: uint64(0), - error: assert.AnError, + error: errors.ErrFailedToFetchResults, }, } @@ -155,10 +157,10 @@ func Test_Users_Create(t *testing.T) { PersonalCode: "123456789", FirstName: "John", LastName: "Doe", - }).Return(nil, assert.AnError) + }).Return(nil, errors.ErrFailedToCreateRecord) }, expected: nil, - error: assert.AnError, + error: errors.ErrFailedToCreateRecord, }, } @@ -240,10 +242,10 @@ func Test_Users_Update(t *testing.T) { PersonalCode: "123456789", FirstName: "John", LastName: "Doe", - }).Return(nil, assert.AnError) + }).Return(nil, errors.ErrFailedToUpdateRecord) }, expected: nil, - error: assert.AnError, + error: errors.ErrFailedToUpdateRecord, }, } @@ -362,10 +364,10 @@ func Test_Users_Delete(t *testing.T) { { name: "Error", before: func() { - repository.EXPECT().Delete(ctx, id).Return(false, assert.AnError) + repository.EXPECT().Delete(ctx, id).Return(false, errors.ErrFailedToDeleteRecord) }, expected: false, - error: assert.AnError, + error: errors.ErrFailedToDeleteRecord, }, } @@ -428,10 +430,10 @@ func Test_Users_FindByIdentityNumber(t *testing.T) { { name: "Error", before: func() { - repository.EXPECT().FindByIdentityNumber(ctx, identityNumber).Return(nil, assert.AnError) + repository.EXPECT().FindByIdentityNumber(ctx, identityNumber).Return(nil, errors.ErrRecordNotFound) }, expected: nil, - error: assert.AnError, + error: errors.ErrRecordNotFound, }, } @@ -524,10 +526,10 @@ func Test_Users_FindUserDetailsById(t *testing.T) { { name: "Error", before: func() { - repository.EXPECT().FindUserDetailsById(ctx, id).Return(nil, assert.AnError) + repository.EXPECT().FindUserDetailsById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, expected: nil, - error: assert.AnError, + error: errors.ErrRecordNotFound, }, } From 211342338038fdb9cbd6062e5816756c2430c2d7 Mon Sep 17 00:00:00 2001 From: tab Date: Fri, 4 Apr 2025 12:56:56 +0300 Subject: [PATCH 08/20] refactor(grpc): Update errors handling --- internal/app/controllers/tokens_test.go | 4 +- internal/app/errors/errors.go | 9 +- internal/app/rpcs/services/permissions.go | 41 +++-- .../app/rpcs/services/permissions_test.go | 150 +++++++++++++----- internal/app/rpcs/services/roles.go | 41 +++-- internal/app/rpcs/services/roles_test.go | 146 ++++++++++++----- internal/app/rpcs/services/scopes.go | 41 +++-- internal/app/rpcs/services/scopes_test.go | 139 +++++++++++----- internal/app/rpcs/services/tokens.go | 14 +- internal/app/rpcs/services/tokens_test.go | 55 +++++-- internal/app/rpcs/services/users.go | 45 ++++-- internal/app/rpcs/services/users_test.go | 147 +++++++++++++---- 12 files changed, 602 insertions(+), 230 deletions(-) diff --git a/internal/app/controllers/tokens_test.go b/internal/app/controllers/tokens_test.go index 0610b07..f47a884 100644 --- a/internal/app/controllers/tokens_test.go +++ b/internal/app/controllers/tokens_test.go @@ -100,9 +100,7 @@ func Test_TokensController_Refresh(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() req := httptest.NewRequest(http.MethodPost, "/api/tokens/refresh", tt.body) w := httptest.NewRecorder() diff --git a/internal/app/errors/errors.go b/internal/app/errors/errors.go index 9115ee3..f56bf8c 100644 --- a/internal/app/errors/errors.go +++ b/internal/app/errors/errors.go @@ -36,9 +36,6 @@ var ( // ErrEmptyDescription indicates that the description is empty or invalid ErrEmptyDescription = errors.New("empty description") - // ErrInvalidAttributes indicates that the provided attributes are invalid - ErrInvalidAttributes = errors.New("invalid attributes") - // ErrInvalidIdentityNumber indicates that the provided identity number is invalid ErrInvalidIdentityNumber = errors.New("invalid identity number") @@ -48,6 +45,9 @@ var ( // ErrSessionNotFound indicates that the requested session could not be found ErrSessionNotFound = errors.New("session not found") + // ErrInvalidArguments indicates that the provided request arguments are invalid + ErrInvalidArguments = errors.New("invalid arguments") + // ErrFailedToFetchResults indicates that failed to fetch results ErrFailedToFetchResults = errors.New("failed to fetch results") @@ -72,9 +72,6 @@ var ( // ErrScopeNotFound indicates that the requested scope could not be found ErrScopeNotFound = errors.New("scope not found") - // ErrTokenNotFound indicates that the requested JSON web token could not be found - ErrTokenNotFound = errors.New("token not found") - // ErrUserNotFound indicates that the requested user could not be found ErrUserNotFound = errors.New("user not found") diff --git a/internal/app/rpcs/services/permissions.go b/internal/app/rpcs/services/permissions.go index 2b54c0c..235aa26 100644 --- a/internal/app/rpcs/services/permissions.go +++ b/internal/app/rpcs/services/permissions.go @@ -32,7 +32,7 @@ func NewPermissions(permissions services.Permissions, log *logger.Logger) proto. //nolint:dupl func (p *permissionsService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListPermissionsResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } pagination := &services.Pagination{ @@ -43,7 +43,13 @@ func (p *permissionsService) List(ctx context.Context, req *proto.PaginatedListR rows, total, err := p.permissions.List(ctx, pagination) if err != nil { p.log.Error().Err(err).Msg("Failed to fetch permissions") - return nil, status.Error(codes.Internal, "failed to fetch permissions") + + switch { + case errors.Is(err, errors.ErrFailedToFetchResults): + return nil, status.Error(codes.Unavailable, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to fetch permissions") + } } collection := make([]*proto.Permission, 0, len(rows)) @@ -67,7 +73,7 @@ func (p *permissionsService) List(ctx context.Context, req *proto.PaginatedListR func (p *permissionsService) Get(ctx context.Context, req *proto.GetPermissionRequest) (*proto.GetPermissionResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -79,10 +85,13 @@ func (p *permissionsService) Get(ctx context.Context, req *proto.GetPermissionRe permission, err := p.permissions.FindById(ctx, id) if err != nil { p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get permission") - if errors.Is(err, errors.ErrPermissionNotFound) { - return nil, status.Error(codes.NotFound, "permission not found") + + switch { + case errors.Is(err, errors.ErrRecordNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to get permission") } - return nil, status.Error(codes.Internal, "failed to get permission") } return &proto.GetPermissionResponse{ @@ -96,7 +105,7 @@ func (p *permissionsService) Get(ctx context.Context, req *proto.GetPermissionRe func (p *permissionsService) Create(ctx context.Context, req *proto.CreatePermissionRequest) (*proto.CreatePermissionResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } permission, err := p.permissions.Create(ctx, &models.Permission{ @@ -105,7 +114,13 @@ func (p *permissionsService) Create(ctx context.Context, req *proto.CreatePermis }) if err != nil { p.log.Error().Err(err).Str("name", req.Name).Msg("Failed to create permission") - return nil, status.Error(codes.Internal, err.Error()) + + switch { + case errors.Is(err, errors.ErrFailedToCreateRecord): + return nil, status.Error(codes.Internal, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to create permission") + } } return &proto.CreatePermissionResponse{ @@ -119,7 +134,7 @@ func (p *permissionsService) Create(ctx context.Context, req *proto.CreatePermis func (p *permissionsService) Update(ctx context.Context, req *proto.UpdatePermissionRequest) (*proto.UpdatePermissionResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -137,8 +152,10 @@ func (p *permissionsService) Update(ctx context.Context, req *proto.UpdatePermis p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update permission") switch { - case errors.Is(err, errors.ErrPermissionNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) + case errors.Is(err, errors.ErrFailedToUpdateRecord): + return nil, status.Error(codes.Internal, err.Error()) default: return nil, status.Error(codes.Internal, "failed to update permission") } @@ -156,7 +173,7 @@ func (p *permissionsService) Update(ctx context.Context, req *proto.UpdatePermis //nolint:dupl func (p *permissionsService) Delete(ctx context.Context, req *proto.DeletePermissionRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -170,7 +187,7 @@ func (p *permissionsService) Delete(ctx context.Context, req *proto.DeletePermis p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete permission") switch { - case errors.Is(err, errors.ErrPermissionNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) default: return nil, status.Error(codes.Internal, "failed to delete permission") diff --git a/internal/app/rpcs/services/permissions_test.go b/internal/app/rpcs/services/permissions_test.go index 77ab52a..43c8f7c 100644 --- a/internal/app/rpcs/services/permissions_test.go +++ b/internal/app/rpcs/services/permissions_test.go @@ -77,7 +77,20 @@ func Test_Permissions_List(t *testing.T) { error: false, }, { - name: "Error", + name: "Invalid Request", + before: func() { + permissions.EXPECT().List(ctx, gomock.Any()).Times(0) + }, + request: &proto.PaginatedListRequest{ + Limit: 0, + Offset: 10, + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Failed to fetch results", before: func() { permissions.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) }, @@ -86,6 +99,19 @@ func Test_Permissions_List(t *testing.T) { Offset: 10, }, expected: nil, + code: codes.Unavailable, + error: true, + }, + { + name: "Error", + before: func() { + permissions.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), assert.AnError) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, code: codes.Internal, error: true, }, @@ -154,10 +180,20 @@ func Test_Permissions_Get(t *testing.T) { }, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.GetPermissionRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - permissions.EXPECT().FindById(ctx, id).Return(nil, errors.ErrPermissionNotFound) + permissions.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, req: &proto.GetPermissionRequest{ Id: id.String(), @@ -167,21 +203,22 @@ func Test_Permissions_Get(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Error", + before: func() { + permissions.EXPECT().FindById(ctx, id).Return(nil, assert.AnError) + }, req: &proto.GetPermissionRequest{ - Id: "invalid-uuid", + Id: id.String(), }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Get(ctx, tt.req) @@ -240,9 +277,20 @@ func Test_Permissions_Create(t *testing.T) { error: false, }, { - name: "Internal Error", + name: "Validation error", + before: func() {}, + req: &proto.CreatePermissionRequest{ + Name: "", + Description: "Read own data", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Error", before: func() { - permissions.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + permissions.EXPECT().Create(ctx, gomock.Any()).Return(nil, errors.ErrFailedToCreateRecord) }, req: &proto.CreatePermissionRequest{ Name: "read:self", @@ -253,22 +301,23 @@ func Test_Permissions_Create(t *testing.T) { error: true, }, { - name: "Validation Error", + name: "Internal error", + before: func() { + permissions.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.CreatePermissionRequest{ - Name: "", + Name: "read:self", Description: "Read own data", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Create(ctx, tt.req) @@ -327,10 +376,33 @@ func Test_Permissions_Update(t *testing.T) { }, error: false, }, + { + name: "Validation error", + before: func() {}, + req: &proto.UpdatePermissionRequest{ + Id: id.String(), + Name: "", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.UpdatePermissionRequest{ + Id: "invalid-uuid", + Name: "read:self", + Description: "Read own data updated", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrPermissionNotFound) + permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrRecordNotFound) }, req: &proto.UpdatePermissionRequest{ Id: id.String(), @@ -342,9 +414,9 @@ func Test_Permissions_Update(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Error", before: func() { - permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrFailedToUpdateRecord) }, req: &proto.UpdatePermissionRequest{ Id: id.String(), @@ -356,23 +428,24 @@ func Test_Permissions_Update(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Internal error", + before: func() { + permissions.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.UpdatePermissionRequest{ - Id: "invalid-uuid", + Id: id.String(), Name: "read:self", Description: "Read own data updated", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Update(ctx, tt.req) @@ -419,10 +492,20 @@ func Test_Permissions_Delete(t *testing.T) { expected: &emptypb.Empty{}, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.DeletePermissionRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - permissions.EXPECT().Delete(ctx, id).Return(false, errors.ErrPermissionNotFound) + permissions.EXPECT().Delete(ctx, id).Return(false, errors.ErrRecordNotFound) }, req: &proto.DeletePermissionRequest{ Id: id.String(), @@ -432,7 +515,7 @@ func Test_Permissions_Delete(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Internal error", before: func() { permissions.EXPECT().Delete(ctx, id).Return(false, assert.AnError) }, @@ -443,22 +526,11 @@ func Test_Permissions_Delete(t *testing.T) { code: codes.Internal, error: true, }, - { - name: "Invalid ID Format", - req: &proto.DeletePermissionRequest{ - Id: "invalid-uuid", - }, - expected: nil, - code: codes.InvalidArgument, - error: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Delete(ctx, tt.req) diff --git a/internal/app/rpcs/services/roles.go b/internal/app/rpcs/services/roles.go index c54b54e..2d3b43c 100644 --- a/internal/app/rpcs/services/roles.go +++ b/internal/app/rpcs/services/roles.go @@ -32,7 +32,7 @@ func NewRoles(roles services.Roles, log *logger.Logger) proto.RoleServiceServer //nolint:dupl func (p *rolesService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListRolesResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } pagination := &services.Pagination{ @@ -43,7 +43,13 @@ func (p *rolesService) List(ctx context.Context, req *proto.PaginatedListRequest rows, total, err := p.roles.List(ctx, pagination) if err != nil { p.log.Error().Err(err).Msg("Failed to fetch roles") - return nil, status.Error(codes.Internal, "failed to fetch roles") + + switch { + case errors.Is(err, errors.ErrFailedToFetchResults): + return nil, status.Error(codes.Unavailable, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to fetch roles") + } } collection := make([]*proto.Role, 0, len(rows)) @@ -67,7 +73,7 @@ func (p *rolesService) List(ctx context.Context, req *proto.PaginatedListRequest func (p *rolesService) Get(ctx context.Context, req *proto.GetRoleRequest) (*proto.GetRoleResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -79,10 +85,13 @@ func (p *rolesService) Get(ctx context.Context, req *proto.GetRoleRequest) (*pro role, err := p.roles.FindById(ctx, id) if err != nil { p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get role") - if errors.Is(err, errors.ErrRoleNotFound) { - return nil, status.Error(codes.NotFound, "role not found") + + switch { + case errors.Is(err, errors.ErrRecordNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to get role") } - return nil, status.Error(codes.Internal, "failed to get role") } return &proto.GetRoleResponse{ @@ -97,7 +106,7 @@ func (p *rolesService) Get(ctx context.Context, req *proto.GetRoleRequest) (*pro func (p *rolesService) Create(ctx context.Context, req *proto.CreateRoleRequest) (*proto.CreateRoleResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } permissionIDs := make([]uuid.UUID, 0, len(req.PermissionIds)) @@ -117,7 +126,13 @@ func (p *rolesService) Create(ctx context.Context, req *proto.CreateRoleRequest) }) if err != nil { p.log.Error().Err(err).Str("name", req.Name).Msg("Failed to create role") - return nil, status.Error(codes.Internal, err.Error()) + + switch { + case errors.Is(err, errors.ErrFailedToCreateRecord): + return nil, status.Error(codes.Internal, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to create role") + } } return &proto.CreateRoleResponse{ @@ -131,7 +146,7 @@ func (p *rolesService) Create(ctx context.Context, req *proto.CreateRoleRequest) func (p *rolesService) Update(ctx context.Context, req *proto.UpdateRoleRequest) (*proto.UpdateRoleResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } permissionIDs := make([]uuid.UUID, 0, len(req.PermissionIds)) @@ -160,8 +175,10 @@ func (p *rolesService) Update(ctx context.Context, req *proto.UpdateRoleRequest) p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update role") switch { - case errors.Is(err, errors.ErrRoleNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) + case errors.Is(err, errors.ErrFailedToUpdateRecord): + return nil, status.Error(codes.Internal, err.Error()) default: return nil, status.Error(codes.Internal, "failed to update role") } @@ -179,7 +196,7 @@ func (p *rolesService) Update(ctx context.Context, req *proto.UpdateRoleRequest) //nolint:dupl func (p *rolesService) Delete(ctx context.Context, req *proto.DeleteRoleRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -193,7 +210,7 @@ func (p *rolesService) Delete(ctx context.Context, req *proto.DeleteRoleRequest) p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete role") switch { - case errors.Is(err, errors.ErrRoleNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) default: return nil, status.Error(codes.Internal, "failed to delete role") diff --git a/internal/app/rpcs/services/roles_test.go b/internal/app/rpcs/services/roles_test.go index dacd9d0..4d9b89d 100644 --- a/internal/app/rpcs/services/roles_test.go +++ b/internal/app/rpcs/services/roles_test.go @@ -87,7 +87,20 @@ func Test_Roles_List(t *testing.T) { error: false, }, { - name: "Error", + name: "Invalid Request", + before: func() { + roles.EXPECT().List(ctx, gomock.Any()).Times(0) + }, + request: &proto.PaginatedListRequest{ + Limit: 0, + Offset: 10, + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Failed to fetch results", before: func() { roles.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) }, @@ -96,6 +109,19 @@ func Test_Roles_List(t *testing.T) { Offset: 10, }, expected: nil, + code: codes.Unavailable, + error: true, + }, + { + name: "Error", + before: func() { + roles.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), assert.AnError) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, code: codes.Internal, error: true, }, @@ -167,7 +193,7 @@ func Test_Roles_Get(t *testing.T) { { name: "Not Found", before: func() { - roles.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRoleNotFound) + roles.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, req: &proto.GetRoleRequest{ Id: id.String(), @@ -177,7 +203,8 @@ func Test_Roles_Get(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Invalid ID format", + before: func() {}, req: &proto.GetRoleRequest{ Id: "invalid-uuid", }, @@ -185,13 +212,23 @@ func Test_Roles_Get(t *testing.T) { code: codes.InvalidArgument, error: true, }, + { + name: "Error", + before: func() { + roles.EXPECT().FindById(ctx, id).Return(nil, assert.AnError) + }, + req: &proto.GetRoleRequest{ + Id: id.String(), + }, + expected: nil, + code: codes.Internal, + error: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Get(ctx, tt.req) @@ -250,9 +287,20 @@ func Test_Roles_Create(t *testing.T) { error: false, }, { - name: "Internal Error", + name: "Validation error", + before: func() {}, + req: &proto.CreateRoleRequest{ + Name: "", + Description: "Admin role", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Error", before: func() { - roles.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + roles.EXPECT().Create(ctx, gomock.Any()).Return(nil, errors.ErrFailedToCreateRecord) }, req: &proto.CreateRoleRequest{ Name: "admin", @@ -263,22 +311,23 @@ func Test_Roles_Create(t *testing.T) { error: true, }, { - name: "Validation Error", + name: "Internal error", + before: func() { + roles.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.CreateRoleRequest{ - Name: "", + Name: "admin", Description: "Admin role", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Create(ctx, tt.req) @@ -337,10 +386,33 @@ func Test_Roles_Update(t *testing.T) { }, error: false, }, + { + name: "Validation error", + before: func() {}, + req: &proto.UpdateRoleRequest{ + Id: id.String(), + Name: "", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.UpdateRoleRequest{ + Id: "invalid-uuid", + Name: "admin", + Description: "Admin role updated", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrRoleNotFound) + roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrRecordNotFound) }, req: &proto.UpdateRoleRequest{ Id: id.String(), @@ -352,9 +424,9 @@ func Test_Roles_Update(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Error", before: func() { - roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrFailedToUpdateRecord) }, req: &proto.UpdateRoleRequest{ Id: id.String(), @@ -366,23 +438,24 @@ func Test_Roles_Update(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Internal error", + before: func() { + roles.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.UpdateRoleRequest{ - Id: "invalid-uuid", + Id: id.String(), Name: "admin", Description: "Admin role updated", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Update(ctx, tt.req) @@ -429,10 +502,20 @@ func Test_Roles_Delete(t *testing.T) { expected: &emptypb.Empty{}, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.DeleteRoleRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - roles.EXPECT().Delete(ctx, id).Return(false, errors.ErrRoleNotFound) + roles.EXPECT().Delete(ctx, id).Return(false, errors.ErrRecordNotFound) }, req: &proto.DeleteRoleRequest{ Id: id.String(), @@ -442,7 +525,7 @@ func Test_Roles_Delete(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Internal error", before: func() { roles.EXPECT().Delete(ctx, id).Return(false, assert.AnError) }, @@ -453,22 +536,11 @@ func Test_Roles_Delete(t *testing.T) { code: codes.Internal, error: true, }, - { - name: "Invalid ID Format", - req: &proto.DeleteRoleRequest{ - Id: "invalid-uuid", - }, - expected: nil, - code: codes.InvalidArgument, - error: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Delete(ctx, tt.req) diff --git a/internal/app/rpcs/services/scopes.go b/internal/app/rpcs/services/scopes.go index f58ab40..18bbe40 100644 --- a/internal/app/rpcs/services/scopes.go +++ b/internal/app/rpcs/services/scopes.go @@ -32,7 +32,7 @@ func NewScopes(scopes services.Scopes, log *logger.Logger) proto.ScopeServiceSer //nolint:dupl func (p *scopesService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListScopesResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } pagination := &services.Pagination{ @@ -43,7 +43,13 @@ func (p *scopesService) List(ctx context.Context, req *proto.PaginatedListReques rows, total, err := p.scopes.List(ctx, pagination) if err != nil { p.log.Error().Err(err).Msg("Failed to fetch scopes") - return nil, status.Error(codes.Internal, "failed to fetch scopes") + + switch { + case errors.Is(err, errors.ErrFailedToFetchResults): + return nil, status.Error(codes.Unavailable, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to fetch scopes") + } } collection := make([]*proto.Scope, 0, len(rows)) @@ -67,7 +73,7 @@ func (p *scopesService) List(ctx context.Context, req *proto.PaginatedListReques func (p *scopesService) Get(ctx context.Context, req *proto.GetScopeRequest) (*proto.GetScopeResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -79,10 +85,13 @@ func (p *scopesService) Get(ctx context.Context, req *proto.GetScopeRequest) (*p scope, err := p.scopes.FindById(ctx, id) if err != nil { p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get scope") - if errors.Is(err, errors.ErrScopeNotFound) { - return nil, status.Error(codes.NotFound, "scope not found") + + switch { + case errors.Is(err, errors.ErrRecordNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to get scope") } - return nil, status.Error(codes.Internal, "failed to get scope") } return &proto.GetScopeResponse{ @@ -96,7 +105,7 @@ func (p *scopesService) Get(ctx context.Context, req *proto.GetScopeRequest) (*p func (p *scopesService) Create(ctx context.Context, req *proto.CreateScopeRequest) (*proto.CreateScopeResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } scope, err := p.scopes.Create(ctx, &models.Scope{ @@ -105,7 +114,13 @@ func (p *scopesService) Create(ctx context.Context, req *proto.CreateScopeReques }) if err != nil { p.log.Error().Err(err).Str("name", req.Name).Msg("Failed to create scope") - return nil, status.Error(codes.Internal, err.Error()) + + switch { + case errors.Is(err, errors.ErrFailedToCreateRecord): + return nil, status.Error(codes.Internal, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to create scope") + } } return &proto.CreateScopeResponse{ @@ -119,7 +134,7 @@ func (p *scopesService) Create(ctx context.Context, req *proto.CreateScopeReques func (p *scopesService) Update(ctx context.Context, req *proto.UpdateScopeRequest) (*proto.UpdateScopeResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -137,8 +152,10 @@ func (p *scopesService) Update(ctx context.Context, req *proto.UpdateScopeReques p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update scope") switch { - case errors.Is(err, errors.ErrScopeNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) + case errors.Is(err, errors.ErrFailedToUpdateRecord): + return nil, status.Error(codes.Internal, err.Error()) default: return nil, status.Error(codes.Internal, "failed to update scope") } @@ -156,7 +173,7 @@ func (p *scopesService) Update(ctx context.Context, req *proto.UpdateScopeReques //nolint:dupl func (p *scopesService) Delete(ctx context.Context, req *proto.DeleteScopeRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -170,7 +187,7 @@ func (p *scopesService) Delete(ctx context.Context, req *proto.DeleteScopeReques p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete scope") switch { - case errors.Is(err, errors.ErrScopeNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) default: return nil, status.Error(codes.Internal, "failed to delete scope") diff --git a/internal/app/rpcs/services/scopes_test.go b/internal/app/rpcs/services/scopes_test.go index 66f5c40..a0e7ef9 100644 --- a/internal/app/rpcs/services/scopes_test.go +++ b/internal/app/rpcs/services/scopes_test.go @@ -77,7 +77,20 @@ func Test_Scopes_List(t *testing.T) { error: false, }, { - name: "Error", + name: "Invalid Request", + before: func() { + scopes.EXPECT().List(ctx, gomock.Any()).Times(0) + }, + request: &proto.PaginatedListRequest{ + Limit: 0, + Offset: 10, + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Failed to fetch results", before: func() { scopes.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) }, @@ -86,6 +99,19 @@ func Test_Scopes_List(t *testing.T) { Offset: 10, }, expected: nil, + code: codes.Unavailable, + error: true, + }, + { + name: "Error", + before: func() { + scopes.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), assert.AnError) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, code: codes.Internal, error: true, }, @@ -154,10 +180,20 @@ func Test_Scopes_Get(t *testing.T) { }, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.GetScopeRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - scopes.EXPECT().FindById(ctx, id).Return(nil, errors.ErrScopeNotFound) + scopes.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, req: &proto.GetScopeRequest{ Id: id.String(), @@ -167,21 +203,22 @@ func Test_Scopes_Get(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Error", + before: func() { + scopes.EXPECT().FindById(ctx, id).Return(nil, assert.AnError) + }, req: &proto.GetScopeRequest{ - Id: "invalid-uuid", + Id: id.String(), }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Get(ctx, tt.req) @@ -240,9 +277,20 @@ func Test_Scopes_Create(t *testing.T) { error: false, }, { - name: "Internal Error", + name: "Validation error", + before: func() {}, + req: &proto.CreateScopeRequest{ + Name: "", + Description: "SSO-service scope", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Error", before: func() { - scopes.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + scopes.EXPECT().Create(ctx, gomock.Any()).Return(nil, errors.ErrFailedToCreateRecord) }, req: &proto.CreateScopeRequest{ Name: "sso-service", @@ -253,22 +301,23 @@ func Test_Scopes_Create(t *testing.T) { error: true, }, { - name: "Validation Error", + name: "Internal error", + before: func() { + scopes.EXPECT().Create(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.CreateScopeRequest{ - Name: "", + Name: "sso-service", Description: "SSO-service scope", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Create(ctx, tt.req) @@ -327,10 +376,22 @@ func Test_Scopes_Update(t *testing.T) { }, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.UpdateScopeRequest{ + Id: "invalid-uuid", + Name: "sso-service", + Description: "SSO-service scope updated", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrScopeNotFound) + scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrRecordNotFound) }, req: &proto.UpdateScopeRequest{ Id: id.String(), @@ -342,9 +403,9 @@ func Test_Scopes_Update(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Error", before: func() { - scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrFailedToUpdateRecord) }, req: &proto.UpdateScopeRequest{ Id: id.String(), @@ -356,23 +417,24 @@ func Test_Scopes_Update(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Internal error", + before: func() { + scopes.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.UpdateScopeRequest{ - Id: "invalid-uuid", + Id: id.String(), Name: "sso-service", Description: "SSO-service scope updated", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Update(ctx, tt.req) @@ -419,10 +481,20 @@ func Test_Scopes_Delete(t *testing.T) { expected: &emptypb.Empty{}, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.DeleteScopeRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - scopes.EXPECT().Delete(ctx, id).Return(false, errors.ErrScopeNotFound) + scopes.EXPECT().Delete(ctx, id).Return(false, errors.ErrRecordNotFound) }, req: &proto.DeleteScopeRequest{ Id: id.String(), @@ -432,7 +504,7 @@ func Test_Scopes_Delete(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Internal error", before: func() { scopes.EXPECT().Delete(ctx, id).Return(false, assert.AnError) }, @@ -443,22 +515,11 @@ func Test_Scopes_Delete(t *testing.T) { code: codes.Internal, error: true, }, - { - name: "Invalid ID Format", - req: &proto.DeleteScopeRequest{ - Id: "invalid-uuid", - }, - expected: nil, - code: codes.InvalidArgument, - error: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Delete(ctx, tt.req) diff --git a/internal/app/rpcs/services/tokens.go b/internal/app/rpcs/services/tokens.go index a0bc4b8..84f0e04 100644 --- a/internal/app/rpcs/services/tokens.go +++ b/internal/app/rpcs/services/tokens.go @@ -32,7 +32,7 @@ func NewTokens(tokens services.Tokens, log *logger.Logger) proto.TokenServiceSer //nolint:dupl func (p *tokensService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListTokensResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } pagination := &services.Pagination{ @@ -43,7 +43,13 @@ func (p *tokensService) List(ctx context.Context, req *proto.PaginatedListReques rows, total, err := p.tokens.List(ctx, pagination) if err != nil { p.log.Error().Err(err).Msg("Failed to fetch tokens") - return nil, status.Error(codes.Internal, "failed to fetch tokens") + + switch { + case errors.Is(err, errors.ErrFailedToFetchResults): + return nil, status.Error(codes.Unavailable, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to fetch tokens") + } } collection := make([]*proto.Token, 0, len(rows)) @@ -70,7 +76,7 @@ func (p *tokensService) List(ctx context.Context, req *proto.PaginatedListReques //nolint:dupl func (p *tokensService) Delete(ctx context.Context, req *proto.DeleteTokenRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -84,7 +90,7 @@ func (p *tokensService) Delete(ctx context.Context, req *proto.DeleteTokenReques p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete token") switch { - case errors.Is(err, errors.ErrTokenNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) default: return nil, status.Error(codes.Internal, "failed to delete token") diff --git a/internal/app/rpcs/services/tokens_test.go b/internal/app/rpcs/services/tokens_test.go index e8425d9..f1aeb78 100644 --- a/internal/app/rpcs/services/tokens_test.go +++ b/internal/app/rpcs/services/tokens_test.go @@ -81,7 +81,20 @@ func Test_Tokens_List(t *testing.T) { error: false, }, { - name: "Error", + name: "Invalid Request", + before: func() { + tokens.EXPECT().List(ctx, gomock.Any()).Times(0) + }, + request: &proto.PaginatedListRequest{ + Limit: 0, + Offset: 10, + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Failed to fetch results", before: func() { tokens.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) }, @@ -90,6 +103,19 @@ func Test_Tokens_List(t *testing.T) { Offset: 10, }, expected: nil, + code: codes.Unavailable, + error: true, + }, + { + name: "Error", + before: func() { + tokens.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), assert.AnError) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, code: codes.Internal, error: true, }, @@ -149,10 +175,20 @@ func Test_Tokens_Delete(t *testing.T) { expected: &emptypb.Empty{}, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.DeleteTokenRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - tokens.EXPECT().Delete(ctx, id).Return(false, errors.ErrTokenNotFound) + tokens.EXPECT().Delete(ctx, id).Return(false, errors.ErrRecordNotFound) }, req: &proto.DeleteTokenRequest{ Id: id.String(), @@ -162,7 +198,7 @@ func Test_Tokens_Delete(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Internal error", before: func() { tokens.EXPECT().Delete(ctx, id).Return(false, assert.AnError) }, @@ -173,22 +209,11 @@ func Test_Tokens_Delete(t *testing.T) { code: codes.Internal, error: true, }, - { - name: "Invalid ID Format", - req: &proto.DeleteTokenRequest{ - Id: "invalid-uuid", - }, - expected: nil, - code: codes.InvalidArgument, - error: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Delete(ctx, tt.req) diff --git a/internal/app/rpcs/services/users.go b/internal/app/rpcs/services/users.go index 809d761..572862f 100644 --- a/internal/app/rpcs/services/users.go +++ b/internal/app/rpcs/services/users.go @@ -2,7 +2,6 @@ package services import ( "context" - "fmt" "github.com/bufbuild/protovalidate-go" "github.com/google/uuid" @@ -33,7 +32,7 @@ func NewUsers(users services.Users, log *logger.Logger) proto.UserServiceServer //nolint:dupl func (p *usersService) List(ctx context.Context, req *proto.PaginatedListRequest) (*proto.ListUsersResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrFailedToFetchResults.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } pagination := &services.Pagination{ @@ -44,7 +43,13 @@ func (p *usersService) List(ctx context.Context, req *proto.PaginatedListRequest rows, total, err := p.users.List(ctx, pagination) if err != nil { p.log.Error().Err(err).Msg("Failed to fetch users") - return nil, status.Error(codes.Internal, "failed to fetch users") + + switch { + case errors.Is(err, errors.ErrFailedToFetchResults): + return nil, status.Error(codes.Unavailable, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to fetch users") + } } collection := make([]*proto.User, 0, len(rows)) @@ -70,7 +75,7 @@ func (p *usersService) List(ctx context.Context, req *proto.PaginatedListRequest func (p *usersService) Get(ctx context.Context, req *proto.GetUserRequest) (*proto.GetUserResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -82,10 +87,13 @@ func (p *usersService) Get(ctx context.Context, req *proto.GetUserRequest) (*pro user, err := p.users.FindById(ctx, id) if err != nil { p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get user") - if errors.Is(err, errors.ErrUserNotFound) { - return nil, status.Error(codes.NotFound, "user not found") + + switch { + case errors.Is(err, errors.ErrRecordNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to get user") } - return nil, status.Error(codes.Internal, "failed to get user") } roleIds := make([]string, 0, len(user.RoleIDs)) @@ -112,11 +120,8 @@ func (p *usersService) Get(ctx context.Context, req *proto.GetUserRequest) (*pro } func (p *usersService) Create(ctx context.Context, req *proto.CreateUserRequest) (*proto.CreateUserResponse, error) { - fmt.Println("--- create ---") - fmt.Println(req) - if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } user, err := p.users.Create(ctx, &models.User{ @@ -127,7 +132,13 @@ func (p *usersService) Create(ctx context.Context, req *proto.CreateUserRequest) }) if err != nil { p.log.Error().Err(err).Str("identity_number", req.IdentityNumber).Msg("Failed to create user") - return nil, status.Error(codes.Internal, err.Error()) + + switch { + case errors.Is(err, errors.ErrFailedToCreateRecord): + return nil, status.Error(codes.Internal, err.Error()) + default: + return nil, status.Error(codes.Internal, "failed to create user") + } } return &proto.CreateUserResponse{ @@ -143,7 +154,7 @@ func (p *usersService) Create(ctx context.Context, req *proto.CreateUserRequest) func (p *usersService) Update(ctx context.Context, req *proto.UpdateUserRequest) (*proto.UpdateUserResponse, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -185,8 +196,10 @@ func (p *usersService) Update(ctx context.Context, req *proto.UpdateUserRequest) p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to update user") switch { - case errors.Is(err, errors.ErrUserNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) + case errors.Is(err, errors.ErrFailedToUpdateRecord): + return nil, status.Error(codes.Internal, err.Error()) default: return nil, status.Error(codes.Internal, "failed to update user") } @@ -206,7 +219,7 @@ func (p *usersService) Update(ctx context.Context, req *proto.UpdateUserRequest) //nolint:dupl func (p *usersService) Delete(ctx context.Context, req *proto.DeleteUserRequest) (*emptypb.Empty, error) { if err := protovalidate.Validate(req); err != nil { - return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidAttributes.Error()) + return nil, status.Error(codes.InvalidArgument, errors.ErrInvalidArguments.Error()) } id, err := uuid.Parse(req.Id) @@ -220,7 +233,7 @@ func (p *usersService) Delete(ctx context.Context, req *proto.DeleteUserRequest) p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to delete user") switch { - case errors.Is(err, errors.ErrUserNotFound): + case errors.Is(err, errors.ErrRecordNotFound): return nil, status.Error(codes.NotFound, err.Error()) default: return nil, status.Error(codes.Internal, "failed to delete user") diff --git a/internal/app/rpcs/services/users_test.go b/internal/app/rpcs/services/users_test.go index 3f90410..149960e 100644 --- a/internal/app/rpcs/services/users_test.go +++ b/internal/app/rpcs/services/users_test.go @@ -85,7 +85,20 @@ func Test_Users_List(t *testing.T) { error: false, }, { - name: "Error", + name: "Invalid Request", + before: func() { + users.EXPECT().List(ctx, gomock.Any()).Times(0) + }, + request: &proto.PaginatedListRequest{ + Limit: 0, + Offset: 10, + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Failed to fetch results", before: func() { users.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), errors.ErrFailedToFetchResults) }, @@ -94,6 +107,19 @@ func Test_Users_List(t *testing.T) { Offset: 10, }, expected: nil, + code: codes.Unavailable, + error: true, + }, + { + name: "Error", + before: func() { + users.EXPECT().List(ctx, gomock.Any()).Return(nil, uint64(0), assert.AnError) + }, + request: &proto.PaginatedListRequest{ + Limit: 1, + Offset: 10, + }, + expected: nil, code: codes.Internal, error: true, }, @@ -168,10 +194,20 @@ func Test_Users_Get(t *testing.T) { }, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.GetUserRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - users.EXPECT().FindById(ctx, id).Return(nil, errors.ErrUserNotFound) + users.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, req: &proto.GetUserRequest{ Id: id.String(), @@ -181,21 +217,22 @@ func Test_Users_Get(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Error", + before: func() { + users.EXPECT().FindById(ctx, id).Return(nil, assert.AnError) + }, req: &proto.GetUserRequest{ - Id: "invalid-uuid", + Id: id.String(), }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Get(ctx, tt.req) @@ -267,7 +304,35 @@ func Test_Users_Create(t *testing.T) { error: false, }, { - name: "Internal Error", + name: "Validation error", + before: func() {}, + req: &proto.CreateUserRequest{ + IdentityNumber: "", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, + { + name: "Error", + before: func() { + users.EXPECT().Create(ctx, gomock.Any()).Return(nil, errors.ErrFailedToCreateRecord) + }, + req: &proto.CreateUserRequest{ + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "EID2016", + LastName: "TESTNUMBER", + }, + expected: nil, + code: codes.Internal, + error: true, + }, + { + name: "Internal error", before: func() { users.EXPECT().Create(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) }, @@ -285,9 +350,7 @@ func Test_Users_Create(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Create(ctx, tt.req) @@ -354,10 +417,24 @@ func Test_Users_Update(t *testing.T) { }, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.UpdateUserRequest{ + Id: "invalid-uuid", + IdentityNumber: "PNOEE-60001017869", + PersonalCode: "60001017869", + FirstName: "JOHN", + LastName: "DOE", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - users.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrUserNotFound) + users.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrRecordNotFound) }, req: &proto.UpdateUserRequest{ Id: id.String(), @@ -371,9 +448,9 @@ func Test_Users_Update(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Error", before: func() { - users.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + users.EXPECT().Update(ctx, gomock.Any()).Return(nil, errors.ErrFailedToUpdateRecord) }, req: &proto.UpdateUserRequest{ Id: id.String(), @@ -387,25 +464,26 @@ func Test_Users_Update(t *testing.T) { error: true, }, { - name: "Invalid ID Format", + name: "Internal error", + before: func() { + users.EXPECT().Update(ctx, gomock.Any()).Return(nil, assert.AnError) + }, req: &proto.UpdateUserRequest{ - Id: "invalid-uuid", + Id: id.String(), IdentityNumber: "PNOEE-60001017869", PersonalCode: "60001017869", FirstName: "JOHN", LastName: "DOE", }, expected: nil, - code: codes.InvalidArgument, + code: codes.Internal, error: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Update(ctx, tt.req) @@ -454,10 +532,20 @@ func Test_Users_Delete(t *testing.T) { expected: &emptypb.Empty{}, error: false, }, + { + name: "Invalid ID format", + before: func() {}, + req: &proto.DeleteUserRequest{ + Id: "invalid-uuid", + }, + expected: nil, + code: codes.InvalidArgument, + error: true, + }, { name: "Not Found", before: func() { - users.EXPECT().Delete(ctx, id).Return(false, errors.ErrUserNotFound) + users.EXPECT().Delete(ctx, id).Return(false, errors.ErrRecordNotFound) }, req: &proto.DeleteUserRequest{ Id: id.String(), @@ -467,7 +555,7 @@ func Test_Users_Delete(t *testing.T) { error: true, }, { - name: "Internal Error", + name: "Internal error", before: func() { users.EXPECT().Delete(ctx, id).Return(false, assert.AnError) }, @@ -478,22 +566,11 @@ func Test_Users_Delete(t *testing.T) { code: codes.Internal, error: true, }, - { - name: "Invalid ID Format", - req: &proto.DeleteUserRequest{ - Id: "invalid-uuid", - }, - expected: nil, - code: codes.InvalidArgument, - error: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.before != nil { - tt.before() - } + tt.before() result, err := service.Delete(ctx, tt.req) From b88d57dd4a25014d436b415529804a19a13414fc Mon Sep 17 00:00:00 2001 From: tab Date: Fri, 4 Apr 2025 18:06:51 +0300 Subject: [PATCH 09/20] chore(codecov): Update codecov ignore patterns --- codecov.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/codecov.yaml b/codecov.yaml index 9108411..242dfa1 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -21,6 +21,7 @@ coverage: ignore: - "internal/app/repositories/db/*.go" + - "internal/app/rpcs/**/*.pb.go" - "**/*_mock.go" - "**/*_test.go" - "**/*.pb.go" From 1249b35462c7abbc6a59bc57c288f4395248f0b9 Mon Sep 17 00:00:00 2001 From: tab Date: Sat, 5 Apr 2025 12:10:31 +0300 Subject: [PATCH 10/20] refactor(config): Remove SECRET_KEY and use RSA keys for JWT signing Updated the JWT implementation to use RSA keys instead of a single secret key for signing --- .env.development | 2 - .env.test | 2 - README.md | 1 - compose.yaml | 1 - docs/installation.md | 1 - internal/app/errors/errors.go | 6 +++ internal/config/config.go | 5 +- internal/config/config_test.go | 2 - pkg/jwt/jwt.go | 82 +++++++++++++++++++++++++---- pkg/jwt/jwt_test.go | 94 +++++++++++++++++++++++++++++----- 10 files changed, 160 insertions(+), 36 deletions(-) diff --git a/.env.development b/.env.development index 7a12ecc..272305d 100644 --- a/.env.development +++ b/.env.development @@ -10,8 +10,6 @@ REDIS_URI=redis://localhost:6379/0 TELEMETRY_URI=localhost:4317 -SECRET_KEY=jwt-secret-key - CERT_PATH=./certs SMART_ID_API_URL=https://sid.demo.sk.ee/smart-id-rp/v2 diff --git a/.env.test b/.env.test index 058b634..189f49d 100644 --- a/.env.test +++ b/.env.test @@ -8,8 +8,6 @@ DATABASE_DSN=postgres://postgres:postgres@localhost:5432/loki-test?sslmode=disab REDIS_URI=redis://localhost:6379/1 -SECRET_KEY=jwt-secret-key - CERT_PATH=./certs SMART_ID_API_URL=https://sid.demo.sk.ee/smart-id-rp/v2 diff --git a/README.md b/README.md index b8a4784..40a546e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,6 @@ docker-compose up Use `.env` files (e.g., `.env.development`) or provide environment variables for: -- `SECRET_KEY` for JWT signing - `DATABASE_DSN` for PostgreSQL - `REDIS_URI` for Redis - `SMART_ID_API_URL`, `MOBILE_ID_API_URL` and corresponding relying on party credentials diff --git a/compose.yaml b/compose.yaml index ae5bc7d..74e9924 100644 --- a/compose.yaml +++ b/compose.yaml @@ -10,7 +10,6 @@ services: - APP_NAME=loki - APP_ADDRESS=0.0.0.0:8080 - CLIENT_URL=http://localhost:3000 - - SECRET_KEY=jwt-secret-key - CERT_PATH=/run/certs - SMART_ID_API_URL=https://sid.demo.sk.ee/smart-id-rp/v2 - SMART_ID_DISPLAY_TEXT=Enter PIN1 diff --git a/docs/installation.md b/docs/installation.md index 1f56cbf..3621b13 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,7 +4,6 @@ Use `.env` files (e.g., `.env.development`) or provide environment variables for: -- `SECRET_KEY` for JWT signing - `DATABASE_DSN` for PostgreSQL - `REDIS_URI` for Redis - `SMART_ID_API_URL`, `MOBILE_ID_API_URL` and corresponding relying on party credentials diff --git a/internal/app/errors/errors.go b/internal/app/errors/errors.go index f56bf8c..5512fed 100644 --- a/internal/app/errors/errors.go +++ b/internal/app/errors/errors.go @@ -6,6 +6,12 @@ var ( // ErrInvalidToken indicates that the provided token is invalid ErrInvalidToken = errors.New("invalid token") + // ErrPrivateKeyNotFound indicates that the private key for signing JWT tokens could not be found + ErrPrivateKeyNotFound = errors.New("failed to load JWT private key") + + // ErrPublicKeyNotFound indicates that the public key for verifying JWT tokens could not be found + ErrPublicKeyNotFound = errors.New("failed to load JWT public key") + // ErrInvalidSigningMethod indicates that an unsupported signing method was used ErrInvalidSigningMethod = errors.New("invalid signing method") diff --git a/internal/config/config.go b/internal/config/config.go index ca026f6..2821d3b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,7 +42,6 @@ type Config struct { AppAddr string GrpcAddr string ClientURL string - SecretKey string CertPath string DatabaseDSN string RedisURI string @@ -70,7 +69,6 @@ func LoadConfig() *Config { flagAppAddr := flag.String("b", AppAddr, "server address") flagGrpcAddr := flag.String("g", GrpcAddr, "gRPC server address") flagClientURL := flag.String("c", ClientURL, "client address") - flagSecretKey := flag.String("s", "", "JWT secret key") flagCertPath := flag.String("p", "", "certificate path") flagDatabaseDSN := flag.String("d", "", "database DSN") flagRedisURI := flag.String("r", "", "Redis URI") @@ -84,8 +82,7 @@ func LoadConfig() *Config { GrpcAddr: getFlagOrEnvString(*flagGrpcAddr, "GRPC_ADDRESS", GrpcAddr), ClientURL: getFlagOrEnvString(*flagClientURL, "CLIENT_URL", ClientURL), - SecretKey: getFlagOrEnvString(*flagSecretKey, "SECRET_KEY", ""), - CertPath: getFlagOrEnvString(*flagCertPath, "CERT_PATH", ""), + CertPath: getFlagOrEnvString(*flagCertPath, "CERT_PATH", ""), DatabaseDSN: getFlagOrEnvString(*flagDatabaseDSN, "DATABASE_DSN", ""), RedisURI: getFlagOrEnvString(*flagRedisURI, "REDIS_URI", ""), diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 4b06426..d72f385 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -40,7 +40,6 @@ func Test_LoadConfig(t *testing.T) { AppAddr: "0.0.0.0:8080", GrpcAddr: "0.0.0.0:50051", ClientURL: "http://localhost:3000", - SecretKey: "jwt-secret-key", CertPath: "./certs", DatabaseDSN: "postgres://postgres:postgres@localhost:5432/loki-test?sslmode=disable", RedisURI: "redis://localhost:6379/1", @@ -75,7 +74,6 @@ func Test_LoadConfig(t *testing.T) { assert.Equal(t, tt.expected.AppAddr, result.AppAddr) assert.Equal(t, tt.expected.GrpcAddr, result.GrpcAddr) assert.Equal(t, tt.expected.ClientURL, result.ClientURL) - assert.Equal(t, tt.expected.SecretKey, result.SecretKey) assert.Equal(t, tt.expected.CertPath, result.CertPath) assert.Equal(t, tt.expected.DatabaseDSN, result.DatabaseDSN) assert.Equal(t, tt.expected.RedisURI, result.RedisURI) diff --git a/pkg/jwt/jwt.go b/pkg/jwt/jwt.go index 694a5ca..c15b477 100644 --- a/pkg/jwt/jwt.go +++ b/pkg/jwt/jwt.go @@ -1,6 +1,9 @@ package jwt import ( + "crypto/rsa" + "os" + "path/filepath" "time" "github.com/golang-jwt/jwt/v5" @@ -9,6 +12,12 @@ import ( "loki/internal/config" ) +const ( + Dir = "jwt" + PrivateKeyFile = "private.key" + PublicKeyFile = "public.key" +) + type Payload struct { ID string `json:"id"` Roles []string `json:"roles,omitempty"` @@ -23,7 +32,9 @@ type Jwt interface { } type jwtService struct { - cfg *config.Config + cfg *config.Config + privateKey *rsa.PrivateKey + publicKey *rsa.PublicKey } type Claims struct { @@ -33,8 +44,17 @@ type Claims struct { Scope []string `json:"scope,omitempty"` } -func NewJWT(cfg *config.Config) Jwt { - return &jwtService{cfg: cfg} +func NewJWT(cfg *config.Config) (Jwt, error) { + privateKey, publicKey, err := loadKeys(cfg) + if err != nil { + return nil, err + } + + return &jwtService{ + cfg: cfg, + privateKey: privateKey, + publicKey: publicKey, + }, nil } func (j *jwtService) Generate(payload Payload, duration time.Duration) (string, error) { @@ -48,9 +68,9 @@ func (j *jwtService) Generate(payload Payload, duration time.Duration) (string, Scope: payload.Scope, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) - signedToken, err := token.SignedString([]byte(j.cfg.SecretKey)) + signedToken, err := token.SignedString(j.privateKey) if err != nil { return "", err } @@ -63,10 +83,10 @@ func (j *jwtService) Verify(token string) (bool, error) { result, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return false, errors.ErrInvalidSigningMethod } - return []byte(j.cfg.SecretKey), nil + return j.publicKey, nil }) if err != nil { @@ -85,10 +105,10 @@ func (j *jwtService) Decode(token string) (*Payload, error) { result, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok { return false, errors.ErrInvalidSigningMethod } - return []byte(j.cfg.SecretKey), nil + return j.publicKey, nil }) if err != nil { @@ -106,3 +126,47 @@ func (j *jwtService) Decode(token string) (*Payload, error) { Scope: claims.Scope, }, nil } + +func loadKeys(cfg *config.Config) (*rsa.PrivateKey, *rsa.PublicKey, error) { + privateKey, err := loadPrivateKey(cfg) + if err != nil { + return nil, nil, errors.ErrPrivateKeyNotFound + } + + publicKey, err := loadPublicKey(cfg) + if err != nil { + return nil, nil, errors.ErrPublicKeyNotFound + } + + return privateKey, publicKey, nil +} + +func loadPrivateKey(cfg *config.Config) (*rsa.PrivateKey, error) { + filePath := filepath.Join(cfg.CertPath, Dir, PrivateKeyFile) + bytes, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + key, err := jwt.ParseRSAPrivateKeyFromPEM(bytes) + if err != nil { + return nil, err + } + + return key, nil +} + +func loadPublicKey(cfg *config.Config) (*rsa.PublicKey, error) { + filePath := filepath.Join(cfg.CertPath, Dir, PublicKeyFile) + bytes, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + key, err := jwt.ParseRSAPublicKeyFromPEM(bytes) + if err != nil { + return nil, err + } + + return key, nil +} diff --git a/pkg/jwt/jwt_test.go b/pkg/jwt/jwt_test.go index 33ea35e..1e6d8ca 100644 --- a/pkg/jwt/jwt_test.go +++ b/pkg/jwt/jwt_test.go @@ -1,10 +1,17 @@ package jwt import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "loki/internal/app/errors" @@ -12,19 +19,25 @@ import ( ) func Test_NewJWT(t *testing.T) { + tempDir := generateTestKeys(t) + cfg := &config.Config{ - SecretKey: "jwt-secret-key", + CertPath: tempDir, } - service := NewJWT(cfg) + service, err := NewJWT(cfg) + assert.NoError(t, err) assert.NotNil(t, service) } func Test_JWT_Generate(t *testing.T) { + tempDir := generateTestKeys(t) + cfg := &config.Config{ - SecretKey: "jwt-secret-key", + CertPath: tempDir, } - service := NewJWT(cfg) + service, err := NewJWT(cfg) + require.NoError(t, err) type result struct { header string @@ -38,10 +51,13 @@ func Test_JWT_Generate(t *testing.T) { { name: "Success", payload: Payload{ - ID: "PNOEE-30303039914", + ID: "PNOEE-30303039914", + Roles: []string{"admin"}, + Permissions: []string{"read:all"}, + Scope: []string{"service-name"}, }, expected: result{ - header: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", + header: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", }, }, { @@ -50,7 +66,7 @@ func Test_JWT_Generate(t *testing.T) { ID: "", }, expected: result{ - header: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9", + header: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", }, }, } @@ -66,10 +82,13 @@ func Test_JWT_Generate(t *testing.T) { } func Test_JWT_Verify(t *testing.T) { + tempDir := generateTestKeys(t) + cfg := &config.Config{ - SecretKey: "jwt-secret-key", + CertPath: tempDir, } - service := NewJWT(cfg) + service, err := NewJWT(cfg) + require.NoError(t, err) tests := []struct { name string @@ -79,7 +98,10 @@ func Test_JWT_Verify(t *testing.T) { { name: "Success", payload: Payload{ - ID: "PNOEE-30303039914", + ID: "PNOEE-30303039914", + Roles: []string{"admin"}, + Permissions: []string{"read:all"}, + Scope: []string{"service-name"}, }, expected: true, }, @@ -165,10 +187,13 @@ func Test_JWT_Verify_Mocked(t *testing.T) { } func Test_JWT_Decode(t *testing.T) { + tempDir := generateTestKeys(t) + cfg := &config.Config{ - SecretKey: "jwt-secret-key", + CertPath: tempDir, } - service := NewJWT(cfg) + service, err := NewJWT(cfg) + require.NoError(t, err) tests := []struct { name string @@ -178,10 +203,16 @@ func Test_JWT_Decode(t *testing.T) { { name: "Success", payload: Payload{ - ID: "PNOEE-30303039914", + ID: "PNOEE-30303039914", + Roles: []string{"admin"}, + Permissions: []string{"read:all"}, + Scope: []string{"service-name"}, }, expected: &Payload{ - ID: "PNOEE-30303039914", + ID: "PNOEE-30303039914", + Roles: []string{"admin"}, + Permissions: []string{"read:all"}, + Scope: []string{"service-name"}, }, }, } @@ -264,3 +295,38 @@ func Test_JWT_Decode_Mocked(t *testing.T) { }) } } + +func generateTestKeys(t *testing.T) string { + tempDir, err := os.MkdirTemp("", "jwt-test-*") + require.NoError(t, err) + + t.Cleanup(func() { os.RemoveAll(tempDir) }) + + jwtDir := filepath.Join(tempDir, Dir) + err = os.MkdirAll(jwtDir, 0755) + require.NoError(t, err) + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + privateKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + }) + + publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + require.NoError(t, err) + publicKeyPEM := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: publicKeyBytes, + }) + + err = os.WriteFile(filepath.Join(jwtDir, PrivateKeyFile), privateKeyPEM, 0600) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(jwtDir, PublicKeyFile), publicKeyPEM, 0644) + require.NoError(t, err) + + return tempDir +} From 51940e3d6a2d7c9f09d929cd535dd54725a71b6f Mon Sep 17 00:00:00 2001 From: tab Date: Sat, 5 Apr 2025 17:11:20 +0300 Subject: [PATCH 11/20] chore(grpc) Use FindRoleDetailsById and FindUserDetailsById methods --- internal/app/rpcs/proto/sso/v1/user.pb.go | 24 +++++++++++-- .../app/rpcs/services/permissions_test.go | 12 ++----- internal/app/rpcs/services/roles.go | 9 +++-- internal/app/rpcs/services/roles_test.go | 36 +++++++++---------- internal/app/rpcs/services/scopes_test.go | 12 ++----- internal/app/rpcs/services/users.go | 2 +- internal/app/rpcs/services/users_test.go | 36 +++++++++---------- 7 files changed, 72 insertions(+), 59 deletions(-) diff --git a/internal/app/rpcs/proto/sso/v1/user.pb.go b/internal/app/rpcs/proto/sso/v1/user.pb.go index 64bf80e..3c388d5 100644 --- a/internal/app/rpcs/proto/sso/v1/user.pb.go +++ b/internal/app/rpcs/proto/sso/v1/user.pb.go @@ -266,6 +266,8 @@ type CreateUserRequest struct { PersonalCode string `protobuf:"bytes,2,opt,name=personal_code,json=personalCode,proto3" json:"personal_code,omitempty"` FirstName string `protobuf:"bytes,3,opt,name=first_name,json=firstName,proto3" json:"first_name,omitempty"` LastName string `protobuf:"bytes,4,opt,name=last_name,json=lastName,proto3" json:"last_name,omitempty"` + RoleIds []string `protobuf:"bytes,5,rep,name=role_ids,json=roleIds,proto3" json:"role_ids,omitempty"` + ScopeIds []string `protobuf:"bytes,6,rep,name=scope_ids,json=scopeIds,proto3" json:"scope_ids,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -328,6 +330,20 @@ func (x *CreateUserRequest) GetLastName() string { return "" } +func (x *CreateUserRequest) GetRoleIds() []string { + if x != nil { + return x.RoleIds + } + return nil +} + +func (x *CreateUserRequest) GetScopeIds() []string { + if x != nil { + return x.ScopeIds + } + return nil +} + // CreateUserResponse is the response for the Create method type CreateUserResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -578,13 +594,17 @@ const file_sso_v1_user_proto_rawDesc = "" + "\x0eGetUserRequest\x12\x18\n" + "\x02id\x18\x01 \x01(\tB\b\xbaH\x05r\x03\xb0\x01\x01R\x02id\"3\n" + "\x0fGetUserResponse\x12 \n" + - "\x04data\x18\x01 \x01(\v2\f.sso.v1.UserR\x04data\"\xc9\x01\n" + + "\x04data\x18\x01 \x01(\v2\f.sso.v1.UserR\x04data\"\x9f\x02\n" + "\x11CreateUserRequest\x122\n" + "\x0fidentity_number\x18\x01 \x01(\tB\t\xbaH\x06r\x04\x10\x0f\x18\x14R\x0eidentityNumber\x12.\n" + "\rpersonal_code\x18\x02 \x01(\tB\t\xbaH\x06r\x04\x10\v\x18\x14R\fpersonalCode\x12(\n" + "\n" + "first_name\x18\x03 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\tfirstName\x12&\n" + - "\tlast_name\x18\x04 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\blastName\"6\n" + + "\tlast_name\x18\x04 \x01(\tB\t\xbaH\x06r\x04\x10\x01\x182R\blastName\x12(\n" + + "\brole_ids\x18\x05 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\aroleIds\x12*\n" + + "\tscope_ids\x18\x06 \x03(\tB\r\xbaH\n" + + "\x92\x01\a\"\x05r\x03\xb0\x01\x01R\bscopeIds\"6\n" + "\x12CreateUserResponse\x12 \n" + "\x04data\x18\x01 \x01(\v2\f.sso.v1.UserR\x04data\"\xb9\x02\n" + "\x11UpdateUserRequest\x12\x18\n" + diff --git a/internal/app/rpcs/services/permissions_test.go b/internal/app/rpcs/services/permissions_test.go index 43c8f7c..17713dd 100644 --- a/internal/app/rpcs/services/permissions_test.go +++ b/internal/app/rpcs/services/permissions_test.go @@ -227,9 +227,7 @@ func Test_Permissions_Get(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } @@ -326,9 +324,7 @@ func Test_Permissions_Create(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } @@ -454,9 +450,7 @@ func Test_Permissions_Update(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } diff --git a/internal/app/rpcs/services/roles.go b/internal/app/rpcs/services/roles.go index 2d3b43c..059a88b 100644 --- a/internal/app/rpcs/services/roles.go +++ b/internal/app/rpcs/services/roles.go @@ -82,7 +82,7 @@ func (p *rolesService) Get(ctx context.Context, req *proto.GetRoleRequest) (*pro return nil, status.Error(codes.InvalidArgument, err.Error()) } - role, err := p.roles.FindById(ctx, id) + role, err := p.roles.FindRoleDetailsById(ctx, id) if err != nil { p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get role") @@ -94,12 +94,17 @@ func (p *rolesService) Get(ctx context.Context, req *proto.GetRoleRequest) (*pro } } + permissionIds := make([]string, 0, len(role.PermissionIDs)) + for _, permissionId := range role.PermissionIDs { + permissionIds = append(permissionIds, permissionId.String()) + } + return &proto.GetRoleResponse{ Data: &proto.Role{ Id: role.ID.String(), Name: role.Name, Description: role.Description, - PermissionIds: []string{}, + PermissionIds: permissionIds, }, }, nil } diff --git a/internal/app/rpcs/services/roles_test.go b/internal/app/rpcs/services/roles_test.go index 4d9b89d..230f33e 100644 --- a/internal/app/rpcs/services/roles_test.go +++ b/internal/app/rpcs/services/roles_test.go @@ -160,6 +160,10 @@ func Test_Roles_Get(t *testing.T) { service := NewRoles(roles, log) id := uuid.MustParse("10000000-1000-1000-1000-000000000001") + permissionIds := []uuid.UUID{ + uuid.MustParse("10000000-1000-1000-3000-000000000001"), + uuid.MustParse("10000000-1000-1000-3000-000000000002"), + } tests := []struct { name string @@ -172,10 +176,11 @@ func Test_Roles_Get(t *testing.T) { { name: "Success", before: func() { - roles.EXPECT().FindById(ctx, id).Return(&models.Role{ - ID: id, - Name: models.AdminRoleType, - Description: "Admin role", + roles.EXPECT().FindRoleDetailsById(ctx, id).Return(&models.Role{ + ID: id, + Name: models.AdminRoleType, + Description: "Admin role", + PermissionIDs: permissionIds, }, nil) }, req: &proto.GetRoleRequest{ @@ -183,9 +188,10 @@ func Test_Roles_Get(t *testing.T) { }, expected: &proto.GetRoleResponse{ Data: &proto.Role{ - Id: id.String(), - Name: "admin", - Description: "Admin role", + Id: id.String(), + Name: "admin", + Description: "Admin role", + PermissionIds: []string{"10000000-1000-1000-3000-000000000001", "10000000-1000-1000-3000-000000000002"}, }, }, error: false, @@ -193,7 +199,7 @@ func Test_Roles_Get(t *testing.T) { { name: "Not Found", before: func() { - roles.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) + roles.EXPECT().FindRoleDetailsById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, req: &proto.GetRoleRequest{ Id: id.String(), @@ -215,7 +221,7 @@ func Test_Roles_Get(t *testing.T) { { name: "Error", before: func() { - roles.EXPECT().FindById(ctx, id).Return(nil, assert.AnError) + roles.EXPECT().FindRoleDetailsById(ctx, id).Return(nil, assert.AnError) }, req: &proto.GetRoleRequest{ Id: id.String(), @@ -237,9 +243,7 @@ func Test_Roles_Get(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } @@ -336,9 +340,7 @@ func Test_Roles_Create(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } @@ -464,9 +466,7 @@ func Test_Roles_Update(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } diff --git a/internal/app/rpcs/services/scopes_test.go b/internal/app/rpcs/services/scopes_test.go index a0e7ef9..8eeadf6 100644 --- a/internal/app/rpcs/services/scopes_test.go +++ b/internal/app/rpcs/services/scopes_test.go @@ -227,9 +227,7 @@ func Test_Scopes_Get(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } @@ -326,9 +324,7 @@ func Test_Scopes_Create(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } @@ -443,9 +439,7 @@ func Test_Scopes_Update(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.Name, result.Data.Name) - assert.Equal(t, tt.expected.Data.Description, result.Data.Description) + assert.Equal(t, tt.expected, result) } }) } diff --git a/internal/app/rpcs/services/users.go b/internal/app/rpcs/services/users.go index 572862f..6be09d7 100644 --- a/internal/app/rpcs/services/users.go +++ b/internal/app/rpcs/services/users.go @@ -84,7 +84,7 @@ func (p *usersService) Get(ctx context.Context, req *proto.GetUserRequest) (*pro return nil, status.Error(codes.InvalidArgument, err.Error()) } - user, err := p.users.FindById(ctx, id) + user, err := p.users.FindUserDetailsById(ctx, id) if err != nil { p.log.Error().Err(err).Str("id", req.Id).Msg("Failed to get user") diff --git a/internal/app/rpcs/services/users_test.go b/internal/app/rpcs/services/users_test.go index 149960e..9f5186c 100644 --- a/internal/app/rpcs/services/users_test.go +++ b/internal/app/rpcs/services/users_test.go @@ -160,6 +160,14 @@ func Test_Users_Get(t *testing.T) { service := NewUsers(users, log) id := uuid.MustParse("10000000-1000-1000-1234-000000000001") + roleIds := []uuid.UUID{ + uuid.MustParse("10000000-1000-1000-1000-000000000001"), + uuid.MustParse("10000000-1000-1000-1000-000000000002"), + } + scopeIds := []uuid.UUID{ + uuid.MustParse("10000000-1000-1000-2000-000000000001"), + uuid.MustParse("10000000-1000-1000-2000-000000000001"), + } tests := []struct { name string @@ -172,12 +180,14 @@ func Test_Users_Get(t *testing.T) { { name: "Success", before: func() { - users.EXPECT().FindById(ctx, id).Return(&models.User{ + users.EXPECT().FindUserDetailsById(ctx, id).Return(&models.User{ ID: id, IdentityNumber: "PNOEE-60001017869", PersonalCode: "60001017869", FirstName: "EID2016", LastName: "TESTNUMBER", + RoleIDs: roleIds, + ScopeIDs: scopeIds, }, nil) }, req: &proto.GetUserRequest{ @@ -190,6 +200,8 @@ func Test_Users_Get(t *testing.T) { PersonalCode: "60001017869", FirstName: "EID2016", LastName: "TESTNUMBER", + RoleIds: []string{"10000000-1000-1000-1000-000000000001", "10000000-1000-1000-1000-000000000002"}, + ScopeIds: []string{"10000000-1000-1000-2000-000000000001", "10000000-1000-1000-2000-000000000001"}, }, }, error: false, @@ -207,7 +219,7 @@ func Test_Users_Get(t *testing.T) { { name: "Not Found", before: func() { - users.EXPECT().FindById(ctx, id).Return(nil, errors.ErrRecordNotFound) + users.EXPECT().FindUserDetailsById(ctx, id).Return(nil, errors.ErrRecordNotFound) }, req: &proto.GetUserRequest{ Id: id.String(), @@ -219,7 +231,7 @@ func Test_Users_Get(t *testing.T) { { name: "Error", before: func() { - users.EXPECT().FindById(ctx, id).Return(nil, assert.AnError) + users.EXPECT().FindUserDetailsById(ctx, id).Return(nil, assert.AnError) }, req: &proto.GetUserRequest{ Id: id.String(), @@ -241,11 +253,7 @@ func Test_Users_Get(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.IdentityNumber, result.Data.IdentityNumber) - assert.Equal(t, tt.expected.Data.PersonalCode, result.Data.PersonalCode) - assert.Equal(t, tt.expected.Data.FirstName, result.Data.FirstName) - assert.Equal(t, tt.expected.Data.LastName, result.Data.LastName) + assert.Equal(t, tt.expected, result) } }) } @@ -359,11 +367,7 @@ func Test_Users_Create(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.IdentityNumber, result.Data.IdentityNumber) - assert.Equal(t, tt.expected.Data.PersonalCode, result.Data.PersonalCode) - assert.Equal(t, tt.expected.Data.FirstName, result.Data.FirstName) - assert.Equal(t, tt.expected.Data.LastName, result.Data.LastName) + assert.Equal(t, tt.expected, result) } }) } @@ -492,11 +496,7 @@ func Test_Users_Update(t *testing.T) { assert.Equal(t, tt.code, st.Code()) } else { assert.NoError(t, err) - assert.Equal(t, tt.expected.Data.Id, result.Data.Id) - assert.Equal(t, tt.expected.Data.IdentityNumber, result.Data.IdentityNumber) - assert.Equal(t, tt.expected.Data.PersonalCode, result.Data.PersonalCode) - assert.Equal(t, tt.expected.Data.FirstName, result.Data.FirstName) - assert.Equal(t, tt.expected.Data.LastName, result.Data.LastName) + assert.Equal(t, tt.expected, result) } }) } From 45aa271aec653ceaf54244ac174d5c5c6274fa8d Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 8 Apr 2025 20:27:22 +0300 Subject: [PATCH 12/20] refactor(logger): JSON structured logging --- internal/app/app.go | 2 +- .../app/rpcs/interceptors/authentication.go | 2 +- .../rpcs/interceptors/authentication_test.go | 12 +- internal/app/rpcs/interceptors/logger.go | 52 ++++ internal/app/rpcs/interceptors/logger_mock.go | 55 ++++ internal/app/rpcs/interceptors/logger_test.go | 58 ++++ internal/app/rpcs/interceptors/module.go | 2 + internal/app/rpcs/interceptors/trace.go | 71 +++++ internal/app/rpcs/interceptors/trace_mock.go | 55 ++++ internal/app/rpcs/interceptors/trace_test.go | 124 +++++++++ internal/app/rpcs/services/permissions.go | 4 +- .../app/rpcs/services/permissions_test.go | 43 ++- internal/app/rpcs/services/roles.go | 4 +- internal/app/rpcs/services/roles_test.go | 43 ++- internal/app/rpcs/services/scopes.go | 4 +- internal/app/rpcs/services/scopes_test.go | 43 ++- internal/app/rpcs/services/tokens.go | 4 +- internal/app/rpcs/services/tokens_test.go | 19 +- internal/app/rpcs/services/users.go | 4 +- internal/app/rpcs/services/users_test.go | 43 ++- internal/app/services/authentication.go | 2 +- .../app/services/authentication/mobileid.go | 2 +- .../services/authentication/mobileid_test.go | 11 +- .../app/services/authentication/module.go | 2 +- .../app/services/authentication/smartid.go | 2 +- .../services/authentication/smartid_test.go | 11 +- internal/app/services/authentication_test.go | 5 +- internal/app/services/permissions.go | 2 +- internal/app/services/permissions_test.go | 43 ++- internal/app/services/roles.go | 2 +- internal/app/services/roles_test.go | 51 +++- internal/app/services/scopes.go | 2 +- internal/app/services/scopes_test.go | 43 ++- internal/app/services/sessions.go | 2 +- internal/app/services/sessions_test.go | 35 ++- internal/app/services/tokens.go | 2 +- internal/app/services/tokens_test.go | 43 ++- internal/app/services/users.go | 2 +- internal/app/services/users_test.go | 59 +++- internal/app/workers/mobileid.go | 2 +- internal/app/workers/mobileid_test.go | 11 +- internal/app/workers/smartid.go | 2 +- internal/app/workers/smartid_test.go | 11 +- internal/config/logger/logger.go | 104 +++++++ internal/config/logger/logger_test.go | 255 ++++++++++++++++++ {pkg => internal/config}/logger/module.go | 0 internal/config/middlewares/authentication.go | 2 +- .../config/middlewares/authentication_test.go | 13 +- internal/config/middlewares/authorization.go | 2 +- internal/config/middlewares/logger.go | 61 +++++ internal/config/middlewares/logger_mock.go | 55 ++++ internal/config/middlewares/logger_test.go | 80 ++++++ internal/config/middlewares/modifier.go | 7 + internal/config/middlewares/modifier_test.go | 28 ++ internal/config/middlewares/module.go | 2 +- internal/config/middlewares/pagination.go | 47 ---- internal/config/middlewares/telemetry.go | 12 +- internal/config/middlewares/utils.go | 5 + internal/config/middlewares/utils_test.go | 35 +++ internal/config/router/router.go | 3 +- internal/config/router/router_test.go | 8 + internal/config/server/grpc.go | 19 +- internal/config/server/grpc_test.go | 30 ++- internal/config/server/server_test.go | 8 + pkg/logger/logger.go | 72 ----- pkg/logger/logger_test.go | 219 --------------- 66 files changed, 1595 insertions(+), 463 deletions(-) create mode 100644 internal/app/rpcs/interceptors/logger.go create mode 100644 internal/app/rpcs/interceptors/logger_mock.go create mode 100644 internal/app/rpcs/interceptors/logger_test.go create mode 100644 internal/app/rpcs/interceptors/trace.go create mode 100644 internal/app/rpcs/interceptors/trace_mock.go create mode 100644 internal/app/rpcs/interceptors/trace_test.go create mode 100644 internal/config/logger/logger.go create mode 100644 internal/config/logger/logger_test.go rename {pkg => internal/config}/logger/module.go (100%) create mode 100644 internal/config/middlewares/logger.go create mode 100644 internal/config/middlewares/logger_mock.go create mode 100644 internal/config/middlewares/logger_test.go delete mode 100644 internal/config/middlewares/pagination.go delete mode 100644 pkg/logger/logger.go delete mode 100644 pkg/logger/logger_test.go diff --git a/internal/app/app.go b/internal/app/app.go index 6d8cc47..b13049a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -17,12 +17,12 @@ import ( "loki/internal/app/services/authentication" "loki/internal/app/workers" "loki/internal/config" + "loki/internal/config/logger" "loki/internal/config/middlewares" "loki/internal/config/router" "loki/internal/config/server" "loki/internal/config/telemetry" "loki/pkg/jwt" - "loki/pkg/logger" ) var Module = fx.Options( diff --git a/internal/app/rpcs/interceptors/authentication.go b/internal/app/rpcs/interceptors/authentication.go index 19130a4..8a34e26 100644 --- a/internal/app/rpcs/interceptors/authentication.go +++ b/internal/app/rpcs/interceptors/authentication.go @@ -8,9 +8,9 @@ import ( "google.golang.org/grpc/status" "loki/internal/app/services" + "loki/internal/config/logger" "loki/internal/config/middlewares" "loki/pkg/jwt" - "loki/pkg/logger" "loki/pkg/rbac" ) diff --git a/internal/app/rpcs/interceptors/authentication_test.go b/internal/app/rpcs/interceptors/authentication_test.go index 59b3f84..e4dcc04 100644 --- a/internal/app/rpcs/interceptors/authentication_test.go +++ b/internal/app/rpcs/interceptors/authentication_test.go @@ -10,8 +10,10 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + + "loki/internal/config" + "loki/internal/config/logger" "loki/pkg/jwt" - "loki/pkg/logger" "loki/internal/app/errors" "loki/internal/app/models" @@ -23,9 +25,15 @@ func Test_AuthenticationInterceptor_Authenticate(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + mockJWT := jwt.NewMockJwt(ctrl) mockUsers := services.NewMockUsers(ctrl) - log := logger.NewLogger() interceptor := NewAuthenticationInterceptor(mockJWT, mockUsers, log) diff --git a/internal/app/rpcs/interceptors/logger.go b/internal/app/rpcs/interceptors/logger.go new file mode 100644 index 0000000..4eb5afa --- /dev/null +++ b/internal/app/rpcs/interceptors/logger.go @@ -0,0 +1,52 @@ +package interceptors + +import ( + "context" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/status" + + "loki/internal/config/logger" +) + +type LoggerInterceptor interface { + Log() grpc.UnaryServerInterceptor +} + +type loggerInterceptor struct { + log *logger.Logger +} + +func NewLoggerInterceptor(log *logger.Logger) LoggerInterceptor { + return &loggerInterceptor{ + log: log, + } +} + +func (i *loggerInterceptor) Log() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + startTime := time.Now() + + traceId := extractTraceId(ctx) + requestId := extractRequestId(ctx) + + reqLogger := i.log. + WithComponent("gRPC"). + WithRequestId(requestId). + WithTraceId(traceId) + + resp, err := handler(ctx, req) + + code := status.Code(err).String() + duration := time.Since(startTime) + + reqLogger.Info(). + Str("method", info.FullMethod). + Str("status", code). + Dur("duration", duration). + Msgf("%s - %s in %s", info.FullMethod, code, duration) + + return resp, err + } +} diff --git a/internal/app/rpcs/interceptors/logger_mock.go b/internal/app/rpcs/interceptors/logger_mock.go new file mode 100644 index 0000000..616f7f1 --- /dev/null +++ b/internal/app/rpcs/interceptors/logger_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/rpcs/interceptors/logger.go +// +// Generated by this command: +// +// mockgen -source=internal/app/rpcs/interceptors/logger.go -destination=internal/app/rpcs/interceptors/logger_mock.go -package=interceptors +// + +// Package interceptors is a generated GoMock package. +package interceptors + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" +) + +// MockLoggerInterceptor is a mock of LoggerInterceptor interface. +type MockLoggerInterceptor struct { + ctrl *gomock.Controller + recorder *MockLoggerInterceptorMockRecorder + isgomock struct{} +} + +// MockLoggerInterceptorMockRecorder is the mock recorder for MockLoggerInterceptor. +type MockLoggerInterceptorMockRecorder struct { + mock *MockLoggerInterceptor +} + +// NewMockLoggerInterceptor creates a new mock instance. +func NewMockLoggerInterceptor(ctrl *gomock.Controller) *MockLoggerInterceptor { + mock := &MockLoggerInterceptor{ctrl: ctrl} + mock.recorder = &MockLoggerInterceptorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoggerInterceptor) EXPECT() *MockLoggerInterceptorMockRecorder { + return m.recorder +} + +// Log mocks base method. +func (m *MockLoggerInterceptor) Log() grpc.UnaryServerInterceptor { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Log") + ret0, _ := ret[0].(grpc.UnaryServerInterceptor) + return ret0 +} + +// Log indicates an expected call of Log. +func (mr *MockLoggerInterceptorMockRecorder) Log() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLoggerInterceptor)(nil).Log)) +} diff --git a/internal/app/rpcs/interceptors/logger_test.go b/internal/app/rpcs/interceptors/logger_test.go new file mode 100644 index 0000000..5790e9b --- /dev/null +++ b/internal/app/rpcs/interceptors/logger_test.go @@ -0,0 +1,58 @@ +package interceptors + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc" + + "loki/internal/config" + "loki/internal/config/logger" +) + +func Test_LoggerInterceptor_UnaryServerInterceptor(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + interceptorInstance := NewLoggerInterceptor(log) + + tests := []struct { + name string + method string + error bool + }{ + { + name: "Success", + method: "/sso.v1.PermissionService/List", + error: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + interceptor := interceptorInstance.Log() + + mockHandler := func(ctx context.Context, req interface{}) (interface{}, error) { + return "test response", nil + } + + resp, err := interceptor( + context.Background(), + "test request", + &grpc.UnaryServerInfo{FullMethod: tt.method}, + mockHandler, + ) + + assert.NoError(t, err) + assert.Equal(t, "test response", resp) + }) + } +} diff --git a/internal/app/rpcs/interceptors/module.go b/internal/app/rpcs/interceptors/module.go index f464603..781450c 100644 --- a/internal/app/rpcs/interceptors/module.go +++ b/internal/app/rpcs/interceptors/module.go @@ -4,4 +4,6 @@ import "go.uber.org/fx" var Module = fx.Options( fx.Provide(NewAuthenticationInterceptor), + fx.Provide(NewTraceInterceptor), + fx.Provide(NewLoggerInterceptor), ) diff --git a/internal/app/rpcs/interceptors/trace.go b/internal/app/rpcs/interceptors/trace.go new file mode 100644 index 0000000..e1babb5 --- /dev/null +++ b/internal/app/rpcs/interceptors/trace.go @@ -0,0 +1,71 @@ +package interceptors + +import ( + "context" + + "github.com/google/uuid" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const ( + RequestId = "X-Request-Id" + TraceId = "X-Trace-Id" +) + +type TraceInterceptor interface { + Trace() grpc.UnaryServerInterceptor +} + +type traceInterceptor struct{} + +func NewTraceInterceptor() TraceInterceptor { + return &traceInterceptor{} +} + +func (i *traceInterceptor) Trace() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + traceId := extractTraceId(ctx) + if traceId == "" { + traceId = uuid.New().String() + } + + requestId := extractRequestId(ctx) + if requestId == "" { + requestId = uuid.New().String() + } + + ctx = metadata.AppendToOutgoingContext(ctx, TraceId, traceId) + ctx = metadata.AppendToOutgoingContext(ctx, RequestId, requestId) + + return handler(ctx, req) + } +} + +func extractTraceId(ctx context.Context) string { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "" + } + + traceId := md.Get(TraceId) + if len(traceId) == 0 { + return "" + } + + return traceId[0] +} + +func extractRequestId(ctx context.Context) string { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "" + } + + requestId := md.Get(RequestId) + if len(requestId) == 0 { + return "" + } + + return requestId[0] +} diff --git a/internal/app/rpcs/interceptors/trace_mock.go b/internal/app/rpcs/interceptors/trace_mock.go new file mode 100644 index 0000000..e7f985a --- /dev/null +++ b/internal/app/rpcs/interceptors/trace_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/app/rpcs/interceptors/trace.go +// +// Generated by this command: +// +// mockgen -source=internal/app/rpcs/interceptors/trace.go -destination=internal/app/rpcs/interceptors/trace_mock.go -package=interceptors +// + +// Package interceptors is a generated GoMock package. +package interceptors + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" +) + +// MockTraceInterceptor is a mock of TraceInterceptor interface. +type MockTraceInterceptor struct { + ctrl *gomock.Controller + recorder *MockTraceInterceptorMockRecorder + isgomock struct{} +} + +// MockTraceInterceptorMockRecorder is the mock recorder for MockTraceInterceptor. +type MockTraceInterceptorMockRecorder struct { + mock *MockTraceInterceptor +} + +// NewMockTraceInterceptor creates a new mock instance. +func NewMockTraceInterceptor(ctrl *gomock.Controller) *MockTraceInterceptor { + mock := &MockTraceInterceptor{ctrl: ctrl} + mock.recorder = &MockTraceInterceptorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTraceInterceptor) EXPECT() *MockTraceInterceptorMockRecorder { + return m.recorder +} + +// Trace mocks base method. +func (m *MockTraceInterceptor) Trace() grpc.UnaryServerInterceptor { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Trace") + ret0, _ := ret[0].(grpc.UnaryServerInterceptor) + return ret0 +} + +// Trace indicates an expected call of Trace. +func (mr *MockTraceInterceptorMockRecorder) Trace() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Trace", reflect.TypeOf((*MockTraceInterceptor)(nil).Trace)) +} diff --git a/internal/app/rpcs/interceptors/trace_test.go b/internal/app/rpcs/interceptors/trace_test.go new file mode 100644 index 0000000..6b00448 --- /dev/null +++ b/internal/app/rpcs/interceptors/trace_test.go @@ -0,0 +1,124 @@ +package interceptors + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +func Test_TraceInterceptor_Trace(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + interceptorInstance := NewTraceInterceptor() + + traceId := uuid.New().String() + requestId := uuid.New().String() + + type result struct { + traceId string + requestId string + } + + tests := []struct { + name string + method string + md metadata.MD + expect result + }{ + { + name: "Success", + method: "/sso.v1.PermissionService/List", + md: metadata.Pairs( + TraceId, traceId, + RequestId, requestId, + ), + expect: result{ + traceId: traceId, + requestId: requestId, + }, + }, + { + name: "No traceId in context", + md: metadata.Pairs( + RequestId, requestId, + ), + expect: result{ + traceId: "", + requestId: requestId, + }, + }, + { + name: "No requestId in context", + md: metadata.Pairs( + TraceId, traceId, + ), + expect: result{ + traceId: traceId, + requestId: "", + }, + }, + { + name: "No traceId and requestId in context", + md: metadata.Pairs(), + expect: result{ + traceId: "", + requestId: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := metadata.NewIncomingContext(context.Background(), tt.md) + + var actualTraceId, actualRequestId string + + mockHandler := func(ctx context.Context, req interface{}) (interface{}, error) { + md, ok := metadata.FromOutgoingContext(ctx) + assert.True(t, ok) + + traceIds := md.Get(TraceId) + assert.NotEmpty(t, traceIds) + actualTraceId = traceIds[0] + + requestIds := md.Get(RequestId) + assert.NotEmpty(t, requestIds) + actualRequestId = requestIds[0] + + return "test response", nil + } + + interceptor := interceptorInstance.Trace() + + resp, err := interceptor( + ctx, + "test request", + &grpc.UnaryServerInfo{FullMethod: tt.method}, + mockHandler, + ) + + assert.NoError(t, err) + assert.Equal(t, "test response", resp) + + if tt.expect.traceId != "" { + assert.Equal(t, tt.expect.traceId, actualTraceId) + } else { + _, err = uuid.Parse(actualTraceId) + assert.NoError(t, err) + } + + if tt.expect.requestId != "" { + assert.Equal(t, tt.expect.requestId, actualRequestId) + } else { + _, err = uuid.Parse(actualRequestId) + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/app/rpcs/services/permissions.go b/internal/app/rpcs/services/permissions.go index 235aa26..e6cc0cd 100644 --- a/internal/app/rpcs/services/permissions.go +++ b/internal/app/rpcs/services/permissions.go @@ -11,9 +11,9 @@ import ( "loki/internal/app/errors" "loki/internal/app/models" - proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" + proto "loki/internal/app/rpcs/proto/sso/v1" ) type permissionsService struct { diff --git a/internal/app/rpcs/services/permissions_test.go b/internal/app/rpcs/services/permissions_test.go index 17713dd..fd8d3ff 100644 --- a/internal/app/rpcs/services/permissions_test.go +++ b/internal/app/rpcs/services/permissions_test.go @@ -15,16 +15,23 @@ import ( "loki/internal/app/models" proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Permissions_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissions := services.NewMockPermissions(ctrl) - log := logger.NewLogger() service := NewPermissions(permissions, log) tests := []struct { @@ -144,9 +151,15 @@ func Test_Permissions_Get(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissions := services.NewMockPermissions(ctrl) - log := logger.NewLogger() service := NewPermissions(permissions, log) id := uuid.MustParse("10000000-1000-1000-3000-000000000001") @@ -237,9 +250,15 @@ func Test_Permissions_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissions := services.NewMockPermissions(ctrl) - log := logger.NewLogger() service := NewPermissions(permissions, log) id := uuid.MustParse("10000000-1000-1000-3000-000000000001") @@ -334,9 +353,15 @@ func Test_Permissions_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissions := services.NewMockPermissions(ctrl) - log := logger.NewLogger() service := NewPermissions(permissions, log) id := uuid.MustParse("10000000-1000-1000-3000-000000000001") @@ -460,9 +485,15 @@ func Test_Permissions_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissions := services.NewMockPermissions(ctrl) - log := logger.NewLogger() service := NewPermissions(permissions, log) id := uuid.MustParse("10000000-1000-1000-3000-000000000001") diff --git a/internal/app/rpcs/services/roles.go b/internal/app/rpcs/services/roles.go index 059a88b..fc2a5ef 100644 --- a/internal/app/rpcs/services/roles.go +++ b/internal/app/rpcs/services/roles.go @@ -11,9 +11,9 @@ import ( "loki/internal/app/errors" "loki/internal/app/models" - proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" + proto "loki/internal/app/rpcs/proto/sso/v1" ) type rolesService struct { diff --git a/internal/app/rpcs/services/roles_test.go b/internal/app/rpcs/services/roles_test.go index 230f33e..e1db9bb 100644 --- a/internal/app/rpcs/services/roles_test.go +++ b/internal/app/rpcs/services/roles_test.go @@ -15,16 +15,23 @@ import ( "loki/internal/app/models" proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Roles_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() roles := services.NewMockRoles(ctrl) - log := logger.NewLogger() service := NewRoles(roles, log) tests := []struct { @@ -154,9 +161,15 @@ func Test_Roles_Get(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() roles := services.NewMockRoles(ctrl) - log := logger.NewLogger() service := NewRoles(roles, log) id := uuid.MustParse("10000000-1000-1000-1000-000000000001") @@ -253,9 +266,15 @@ func Test_Roles_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() roles := services.NewMockRoles(ctrl) - log := logger.NewLogger() service := NewRoles(roles, log) id := uuid.MustParse("10000000-1000-1000-1000-000000000001") @@ -350,9 +369,15 @@ func Test_Roles_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() roles := services.NewMockRoles(ctrl) - log := logger.NewLogger() service := NewRoles(roles, log) id := uuid.MustParse("10000000-1000-1000-1000-000000000001") @@ -476,9 +501,15 @@ func Test_Roles_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() roles := services.NewMockRoles(ctrl) - log := logger.NewLogger() service := NewRoles(roles, log) id := uuid.MustParse("10000000-1000-1000-1000-000000000001") diff --git a/internal/app/rpcs/services/scopes.go b/internal/app/rpcs/services/scopes.go index 18bbe40..124dfc1 100644 --- a/internal/app/rpcs/services/scopes.go +++ b/internal/app/rpcs/services/scopes.go @@ -11,9 +11,9 @@ import ( "loki/internal/app/errors" "loki/internal/app/models" - proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" + proto "loki/internal/app/rpcs/proto/sso/v1" ) type scopesService struct { diff --git a/internal/app/rpcs/services/scopes_test.go b/internal/app/rpcs/services/scopes_test.go index 8eeadf6..3a7e163 100644 --- a/internal/app/rpcs/services/scopes_test.go +++ b/internal/app/rpcs/services/scopes_test.go @@ -15,16 +15,23 @@ import ( "loki/internal/app/models" proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Scopes_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() scopes := services.NewMockScopes(ctrl) - log := logger.NewLogger() service := NewScopes(scopes, log) tests := []struct { @@ -144,9 +151,15 @@ func Test_Scopes_Get(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() scopes := services.NewMockScopes(ctrl) - log := logger.NewLogger() service := NewScopes(scopes, log) id := uuid.MustParse("10000000-1000-1000-2000-000000000001") @@ -237,9 +250,15 @@ func Test_Scopes_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() scopes := services.NewMockScopes(ctrl) - log := logger.NewLogger() service := NewScopes(scopes, log) id := uuid.MustParse("10000000-1000-1000-2000-000000000001") @@ -334,9 +353,15 @@ func Test_Scopes_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() scopes := services.NewMockScopes(ctrl) - log := logger.NewLogger() service := NewScopes(scopes, log) id := uuid.MustParse("10000000-1000-1000-2000-000000000001") @@ -449,9 +474,15 @@ func Test_Scopes_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() scopes := services.NewMockScopes(ctrl) - log := logger.NewLogger() service := NewScopes(scopes, log) id := uuid.MustParse("10000000-1000-1000-2000-000000000001") diff --git a/internal/app/rpcs/services/tokens.go b/internal/app/rpcs/services/tokens.go index 84f0e04..fa11501 100644 --- a/internal/app/rpcs/services/tokens.go +++ b/internal/app/rpcs/services/tokens.go @@ -11,9 +11,9 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" "loki/internal/app/errors" - proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" + proto "loki/internal/app/rpcs/proto/sso/v1" ) type tokensService struct { diff --git a/internal/app/rpcs/services/tokens_test.go b/internal/app/rpcs/services/tokens_test.go index f1aeb78..c5e49fb 100644 --- a/internal/app/rpcs/services/tokens_test.go +++ b/internal/app/rpcs/services/tokens_test.go @@ -15,16 +15,23 @@ import ( "loki/internal/app/models" proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Tokens_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() tokens := services.NewMockTokens(ctrl) - log := logger.NewLogger() service := NewTokens(tokens, log) tests := []struct { @@ -149,9 +156,15 @@ func Test_Tokens_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() tokens := services.NewMockTokens(ctrl) - log := logger.NewLogger() service := NewTokens(tokens, log) id := uuid.MustParse("10000000-1000-1000-6000-000000000001") diff --git a/internal/app/rpcs/services/users.go b/internal/app/rpcs/services/users.go index 6be09d7..9667e3e 100644 --- a/internal/app/rpcs/services/users.go +++ b/internal/app/rpcs/services/users.go @@ -11,9 +11,9 @@ import ( "loki/internal/app/errors" "loki/internal/app/models" - proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" + proto "loki/internal/app/rpcs/proto/sso/v1" ) type usersService struct { diff --git a/internal/app/rpcs/services/users_test.go b/internal/app/rpcs/services/users_test.go index 9f5186c..2afd17f 100644 --- a/internal/app/rpcs/services/users_test.go +++ b/internal/app/rpcs/services/users_test.go @@ -15,16 +15,23 @@ import ( "loki/internal/app/models" proto "loki/internal/app/rpcs/proto/sso/v1" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Users_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() users := services.NewMockUsers(ctrl) - log := logger.NewLogger() service := NewUsers(users, log) tests := []struct { @@ -154,9 +161,15 @@ func Test_Users_Get(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() users := services.NewMockUsers(ctrl) - log := logger.NewLogger() service := NewUsers(users, log) id := uuid.MustParse("10000000-1000-1000-1234-000000000001") @@ -263,9 +276,15 @@ func Test_Users_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() users := services.NewMockUsers(ctrl) - log := logger.NewLogger() service := NewUsers(users, log) id := uuid.MustParse("10000000-1000-1000-1234-000000000001") @@ -377,9 +396,15 @@ func Test_Users_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() users := services.NewMockUsers(ctrl) - log := logger.NewLogger() service := NewUsers(users, log) id := uuid.MustParse("10000000-1000-1000-1234-000000000001") @@ -506,9 +531,15 @@ func Test_Users_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() users := services.NewMockUsers(ctrl) - log := logger.NewLogger() service := NewUsers(users, log) id := uuid.MustParse("10000000-1000-1000-1234-000000000001") diff --git a/internal/app/services/authentication.go b/internal/app/services/authentication.go index 2103127..e77ca39 100644 --- a/internal/app/services/authentication.go +++ b/internal/app/services/authentication.go @@ -5,7 +5,7 @@ import ( "loki/internal/app/models" "loki/internal/config" - "loki/pkg/logger" + "loki/internal/config/logger" ) const AuthenticationSuccess = "SUCCESS" diff --git a/internal/app/services/authentication/mobileid.go b/internal/app/services/authentication/mobileid.go index e2685c9..c73fd85 100644 --- a/internal/app/services/authentication/mobileid.go +++ b/internal/app/services/authentication/mobileid.go @@ -10,7 +10,7 @@ import ( "loki/internal/app/models/dto" "loki/internal/app/services" "loki/internal/app/workers" - "loki/pkg/logger" + "loki/internal/config/logger" ) type MobileIdProvider interface { diff --git a/internal/app/services/authentication/mobileid_test.go b/internal/app/services/authentication/mobileid_test.go index 34a8c50..9f10fb9 100644 --- a/internal/app/services/authentication/mobileid_test.go +++ b/internal/app/services/authentication/mobileid_test.go @@ -13,19 +13,26 @@ import ( "loki/internal/app/models/dto" "loki/internal/app/services" "loki/internal/app/workers" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_MobileId_CreateSession(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() clientMock := mobileid.NewMockClient(ctrl) sessionsMock := services.NewMockSessions(ctrl) usersMock := services.NewMockUsers(ctrl) workerMock := workers.NewMockMobileIdWorker(ctrl) - log := logger.NewLogger() service := NewMobileId(clientMock, sessionsMock, usersMock, workerMock, log) diff --git a/internal/app/services/authentication/module.go b/internal/app/services/authentication/module.go index 9a95ade..730efbd 100644 --- a/internal/app/services/authentication/module.go +++ b/internal/app/services/authentication/module.go @@ -8,7 +8,7 @@ import ( "go.uber.org/fx" "loki/internal/config" - "loki/pkg/logger" + "loki/internal/config/logger" ) const ( diff --git a/internal/app/services/authentication/smartid.go b/internal/app/services/authentication/smartid.go index 53626e2..96d4c42 100644 --- a/internal/app/services/authentication/smartid.go +++ b/internal/app/services/authentication/smartid.go @@ -10,7 +10,7 @@ import ( "loki/internal/app/models/dto" "loki/internal/app/services" "loki/internal/app/workers" - "loki/pkg/logger" + "loki/internal/config/logger" ) type SmartIdProvider interface { diff --git a/internal/app/services/authentication/smartid_test.go b/internal/app/services/authentication/smartid_test.go index a9fad07..7349c76 100644 --- a/internal/app/services/authentication/smartid_test.go +++ b/internal/app/services/authentication/smartid_test.go @@ -13,19 +13,26 @@ import ( "loki/internal/app/models/dto" "loki/internal/app/services" "loki/internal/app/workers" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_SmartId_CreateSession(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() clientMock := smartid.NewMockClient(ctrl) sessionsMock := services.NewMockSessions(ctrl) usersMock := services.NewMockUsers(ctrl) workerMock := workers.NewMockSmartIdWorker(ctrl) - log := logger.NewLogger() service := NewSmartId(clientMock, sessionsMock, usersMock, workerMock, log) diff --git a/internal/app/services/authentication_test.go b/internal/app/services/authentication_test.go index 46373d3..13f9aa2 100644 --- a/internal/app/services/authentication_test.go +++ b/internal/app/services/authentication_test.go @@ -10,7 +10,7 @@ import ( "loki/internal/app/models" "loki/internal/config" - "loki/pkg/logger" + "loki/internal/config/logger" ) func Test_Authentication_Complete(t *testing.T) { @@ -31,10 +31,11 @@ func Test_Authentication_Complete(t *testing.T) { RelyingPartyUUID: "00000000-0000-0000-0000-000000000000", Text: "Enter PIN1", }, + LogLevel: "info", } sessionsService := NewMockSessions(ctrl) tokensService := NewMockTokens(ctrl) - log := logger.NewLogger() + log := logger.NewLogger(cfg) id := uuid.MustParse("5eab0e6a-c3e7-4526-a47e-398f0d31f514") sessionId := id.String() diff --git a/internal/app/services/permissions.go b/internal/app/services/permissions.go index 67c059c..4d1e1ed 100644 --- a/internal/app/services/permissions.go +++ b/internal/app/services/permissions.go @@ -9,7 +9,7 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config/logger" ) type Permissions interface { diff --git a/internal/app/services/permissions_test.go b/internal/app/services/permissions_test.go index a3b0e61..6ef5a0b 100644 --- a/internal/app/services/permissions_test.go +++ b/internal/app/services/permissions_test.go @@ -12,16 +12,23 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Permissions_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockPermissionRepository(ctrl) - log := logger.NewLogger() service := NewPermissions(repository, log) tests := []struct { @@ -99,9 +106,15 @@ func Test_Permissions_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockPermissionRepository(ctrl) - log := logger.NewLogger() service := NewPermissions(repository, log) tests := []struct { @@ -165,9 +178,15 @@ func Test_Permissions_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockPermissionRepository(ctrl) - log := logger.NewLogger() service := NewPermissions(repository, log) tests := []struct { @@ -234,9 +253,15 @@ func Test_Permissions_FindById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockPermissionRepository(ctrl) - log := logger.NewLogger() service := NewPermissions(repository, log) tests := []struct { @@ -291,9 +316,15 @@ func Test_Permissions_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockPermissionRepository(ctrl) - log := logger.NewLogger() service := NewPermissions(repository, log) id := uuid.MustParse("10000000-1000-1000-3000-000000000001") diff --git a/internal/app/services/roles.go b/internal/app/services/roles.go index ff3606a..815c348 100644 --- a/internal/app/services/roles.go +++ b/internal/app/services/roles.go @@ -9,7 +9,7 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config/logger" ) type Roles interface { diff --git a/internal/app/services/roles_test.go b/internal/app/services/roles_test.go index d5b53c8..8319a5f 100644 --- a/internal/app/services/roles_test.go +++ b/internal/app/services/roles_test.go @@ -12,16 +12,23 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Roles_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockRoleRepository(ctrl) - log := logger.NewLogger() service := NewRoles(repository, log) tests := []struct { @@ -109,9 +116,15 @@ func Test_Roles_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockRoleRepository(ctrl) - log := logger.NewLogger() service := NewRoles(repository, log) tests := []struct { @@ -181,9 +194,15 @@ func Test_Roles_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockRoleRepository(ctrl) - log := logger.NewLogger() service := NewRoles(repository, log) tests := []struct { @@ -250,9 +269,15 @@ func Test_Roles_FindById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockRoleRepository(ctrl) - log := logger.NewLogger() service := NewRoles(repository, log) tests := []struct { @@ -307,9 +332,15 @@ func Test_Roles_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockRoleRepository(ctrl) - log := logger.NewLogger() service := NewRoles(repository, log) id := uuid.MustParse("10000000-1000-1000-1000-000000000001") @@ -358,9 +389,15 @@ func Test_Roles_FindRoleDetailsById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockRoleRepository(ctrl) - log := logger.NewLogger() service := NewRoles(repository, log) tests := []struct { diff --git a/internal/app/services/scopes.go b/internal/app/services/scopes.go index ae93125..80d89e9 100644 --- a/internal/app/services/scopes.go +++ b/internal/app/services/scopes.go @@ -9,7 +9,7 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config/logger" ) type Scopes interface { diff --git a/internal/app/services/scopes_test.go b/internal/app/services/scopes_test.go index 939ba75..e42b7f6 100644 --- a/internal/app/services/scopes_test.go +++ b/internal/app/services/scopes_test.go @@ -12,16 +12,23 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Scopes_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockScopeRepository(ctrl) - log := logger.NewLogger() service := NewScopes(repository, log) tests := []struct { @@ -99,9 +106,15 @@ func Test_Scopes_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockScopeRepository(ctrl) - log := logger.NewLogger() service := NewScopes(repository, log) tests := []struct { @@ -171,9 +184,15 @@ func Test_Scopes_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockScopeRepository(ctrl) - log := logger.NewLogger() service := NewScopes(repository, log) tests := []struct { @@ -240,9 +259,15 @@ func Test_Scopes_FindById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockScopeRepository(ctrl) - log := logger.NewLogger() service := NewScopes(repository, log) tests := []struct { @@ -297,9 +322,15 @@ func Test_Scopes_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockScopeRepository(ctrl) - log := logger.NewLogger() service := NewScopes(repository, log) id := uuid.MustParse("10000000-1000-1000-2000-000000000001") diff --git a/internal/app/services/sessions.go b/internal/app/services/sessions.go index 1949f3d..611fa8e 100644 --- a/internal/app/services/sessions.go +++ b/internal/app/services/sessions.go @@ -7,7 +7,7 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" - "loki/pkg/logger" + "loki/internal/config/logger" ) type Sessions interface { diff --git a/internal/app/services/sessions_test.go b/internal/app/services/sessions_test.go index 2871a10..f61bd21 100644 --- a/internal/app/services/sessions_test.go +++ b/internal/app/services/sessions_test.go @@ -10,16 +10,23 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Sessions_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockSessionRepository(ctrl) - log := logger.NewLogger() service := NewSessions(repository, log) id := uuid.MustParse("5eab0e6a-c3e7-4526-a47e-398f0d31f514") @@ -90,9 +97,15 @@ func Test_Authentication_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockSessionRepository(ctrl) - log := logger.NewLogger() service := NewSessions(repository, log) id := uuid.MustParse("5eab0e6a-c3e7-4526-a47e-398f0d31f514") @@ -159,9 +172,15 @@ func Test_Sessions_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockSessionRepository(ctrl) - log := logger.NewLogger() service := NewSessions(repository, log) id := uuid.MustParse("5eab0e6a-c3e7-4526-a47e-398f0d31f514") @@ -207,9 +226,15 @@ func Test_Sessions_FindById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockSessionRepository(ctrl) - log := logger.NewLogger() service := NewSessions(repository, log) id := uuid.MustParse("5eab0e6a-c3e7-4526-a47e-398f0d31f514") diff --git a/internal/app/services/tokens.go b/internal/app/services/tokens.go index f257101..7244830 100644 --- a/internal/app/services/tokens.go +++ b/internal/app/services/tokens.go @@ -10,7 +10,7 @@ import ( "loki/internal/app/repositories" "loki/internal/app/repositories/db" "loki/pkg/jwt" - "loki/pkg/logger" + "loki/internal/config/logger" ) type Tokens interface { diff --git a/internal/app/services/tokens_test.go b/internal/app/services/tokens_test.go index 6e9714e..4f2941c 100644 --- a/internal/app/services/tokens_test.go +++ b/internal/app/services/tokens_test.go @@ -11,14 +11,22 @@ import ( "loki/internal/app/errors" "loki/internal/app/models" "loki/internal/app/repositories" + "loki/internal/config" + "loki/internal/config/logger" "loki/pkg/jwt" - "loki/pkg/logger" ) func Test_Tokens_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissionRepository := repositories.NewMockPermissionRepository(ctrl) roleRepository := repositories.NewMockRoleRepository(ctrl) @@ -27,7 +35,6 @@ func Test_Tokens_List(t *testing.T) { userRepository := repositories.NewMockUserRepository(ctrl) jwtService := jwt.NewMockJwt(ctrl) - log := logger.NewLogger() service := NewTokens( jwtService, permissionRepository, @@ -91,6 +98,13 @@ func Test_Tokens_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissionRepository := repositories.NewMockPermissionRepository(ctrl) roleRepository := repositories.NewMockRoleRepository(ctrl) @@ -99,7 +113,6 @@ func Test_Tokens_Create(t *testing.T) { userRepository := repositories.NewMockUserRepository(ctrl) jwtService := jwt.NewMockJwt(ctrl) - log := logger.NewLogger() service := NewTokens( jwtService, permissionRepository, @@ -283,6 +296,13 @@ func Test_Tokens_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissionRepository := repositories.NewMockPermissionRepository(ctrl) roleRepository := repositories.NewMockRoleRepository(ctrl) @@ -291,7 +311,6 @@ func Test_Tokens_Update(t *testing.T) { userRepository := repositories.NewMockUserRepository(ctrl) jwtService := jwt.NewMockJwt(ctrl) - log := logger.NewLogger() service := NewTokens( jwtService, permissionRepository, @@ -491,6 +510,13 @@ func Test_Tokens_FindById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissionRepository := repositories.NewMockPermissionRepository(ctrl) roleRepository := repositories.NewMockRoleRepository(ctrl) @@ -499,7 +525,6 @@ func Test_Tokens_FindById(t *testing.T) { userRepository := repositories.NewMockUserRepository(ctrl) jwtService := jwt.NewMockJwt(ctrl) - log := logger.NewLogger() service := NewTokens( jwtService, permissionRepository, @@ -558,6 +583,13 @@ func Test_Tokens_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() permissionRepository := repositories.NewMockPermissionRepository(ctrl) roleRepository := repositories.NewMockRoleRepository(ctrl) @@ -566,7 +598,6 @@ func Test_Tokens_Delete(t *testing.T) { userRepository := repositories.NewMockUserRepository(ctrl) jwtService := jwt.NewMockJwt(ctrl) - log := logger.NewLogger() service := NewTokens( jwtService, permissionRepository, diff --git a/internal/app/services/users.go b/internal/app/services/users.go index db8857a..ac2922a 100644 --- a/internal/app/services/users.go +++ b/internal/app/services/users.go @@ -9,7 +9,7 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config/logger" ) type Users interface { diff --git a/internal/app/services/users_test.go b/internal/app/services/users_test.go index ec2310f..f411f4d 100644 --- a/internal/app/services/users_test.go +++ b/internal/app/services/users_test.go @@ -12,16 +12,23 @@ import ( "loki/internal/app/models" "loki/internal/app/repositories" "loki/internal/app/repositories/db" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_Users_List(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) tests := []struct { @@ -107,9 +114,15 @@ func Test_Users_Create(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) id, err := uuid.NewRandom() @@ -190,9 +203,15 @@ func Test_Users_Update(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) id, err := uuid.NewRandom() @@ -276,9 +295,15 @@ func Test_Users_FindById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) id, err := uuid.NewRandom() @@ -340,9 +365,15 @@ func Test_Users_Delete(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) id, err := uuid.NewRandom() @@ -392,9 +423,15 @@ func Test_Users_FindByIdentityNumber(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) id, err := uuid.NewRandom() @@ -458,9 +495,15 @@ func Test_Users_FindUserDetailsById(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() repository := repositories.NewMockUserRepository(ctrl) - log := logger.NewLogger() service := NewUsers(repository, log) id, err := uuid.NewRandom() diff --git a/internal/app/workers/mobileid.go b/internal/app/workers/mobileid.go index 3f48384..be078c0 100644 --- a/internal/app/workers/mobileid.go +++ b/internal/app/workers/mobileid.go @@ -11,7 +11,7 @@ import ( "loki/internal/app/models" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" ) type MobileIdWorker interface { diff --git a/internal/app/workers/mobileid_test.go b/internal/app/workers/mobileid_test.go index 4be480f..0ab6664 100644 --- a/internal/app/workers/mobileid_test.go +++ b/internal/app/workers/mobileid_test.go @@ -11,18 +11,25 @@ import ( "loki/internal/app/models" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_MobileIdWorker_Perform(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() sessionsMock := services.NewMockSessions(ctrl) usersMock := services.NewMockUsers(ctrl) workerMock := mobileid.NewMockWorker(ctrl) - log := logger.NewLogger() worker := NewMobileIdWorker(sessionsMock, usersMock, workerMock, log) diff --git a/internal/app/workers/smartid.go b/internal/app/workers/smartid.go index efc7257..633a18c 100644 --- a/internal/app/workers/smartid.go +++ b/internal/app/workers/smartid.go @@ -11,7 +11,7 @@ import ( "loki/internal/app/models" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config/logger" ) type SmartIdWorker interface { diff --git a/internal/app/workers/smartid_test.go b/internal/app/workers/smartid_test.go index 9f9b88b..4142de3 100644 --- a/internal/app/workers/smartid_test.go +++ b/internal/app/workers/smartid_test.go @@ -11,18 +11,25 @@ import ( "loki/internal/app/models" "loki/internal/app/services" - "loki/pkg/logger" + "loki/internal/config" + "loki/internal/config/logger" ) func Test_SmartIdWorker_Perform(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + ctx := context.Background() sessionsMock := services.NewMockSessions(ctrl) usersMock := services.NewMockUsers(ctrl) workerMock := smartid.NewMockWorker(ctrl) - log := logger.NewLogger() worker := NewSmartIdWorker(sessionsMock, usersMock, workerMock, log) diff --git a/internal/config/logger/logger.go b/internal/config/logger/logger.go new file mode 100644 index 0000000..a00547a --- /dev/null +++ b/internal/config/logger/logger.go @@ -0,0 +1,104 @@ +package logger + +import ( + "io" + "os" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/pkgerrors" + + "loki/internal/config" +) + +const ( + Component = "component" + RequestId = "request_id" + TraceId = "trace_id" + + DebugLevel = "debug" + InfoLevel = "info" + WarnLevel = "warn" + ErrorLevel = "error" + FatalLevel = "fatal" + PanicLevel = "panic" + TraceLevel = "trace" +) + +type Logger struct { + log zerolog.Logger +} + +func NewLogger(cfg *config.Config) *Logger { + zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack + zerolog.TimeFieldFormat = "2006-01-02T15:04:05.000Z" + + var output io.Writer = os.Stdout + + hostname, _ := os.Hostname() + + log := zerolog.New(output). + Level(getLogLevel(cfg.LogLevel)). + With(). + Timestamp(). + Str("service", cfg.AppName). + Str("environment", cfg.AppEnv). + Str("host", hostname). + Logger() + + return &Logger{log: log} +} + +func (l *Logger) WithComponent(component string) *Logger { + return &Logger{ + log: l.log.With().Str(Component, component).Logger(), + } +} + +func (l *Logger) WithRequestId(requestId string) *Logger { + return &Logger{ + log: l.log.With().Str(RequestId, requestId).Logger(), + } +} + +func (l *Logger) WithTraceId(traceId string) *Logger { + return &Logger{ + log: l.log.With().Str(TraceId, traceId).Logger(), + } +} + +func (l *Logger) Debug() *zerolog.Event { + return l.log.Debug() +} + +func (l *Logger) Info() *zerolog.Event { + return l.log.Info() +} + +func (l *Logger) Warn() *zerolog.Event { + return l.log.Warn() +} + +func (l *Logger) Error() *zerolog.Event { + return l.log.Error() +} + +func getLogLevel(level string) zerolog.Level { + switch level { + case DebugLevel: + return zerolog.DebugLevel + case InfoLevel: + return zerolog.InfoLevel + case WarnLevel: + return zerolog.WarnLevel + case ErrorLevel: + return zerolog.ErrorLevel + case FatalLevel: + return zerolog.FatalLevel + case PanicLevel: + return zerolog.PanicLevel + case TraceLevel: + return zerolog.TraceLevel + default: + return zerolog.InfoLevel + } +} diff --git a/internal/config/logger/logger_test.go b/internal/config/logger/logger_test.go new file mode 100644 index 0000000..828c0ef --- /dev/null +++ b/internal/config/logger/logger_test.go @@ -0,0 +1,255 @@ +package logger + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + + "loki/internal/config" +) + +func Test_NewLogger(t *testing.T) { + tests := []struct { + name string + cfg *config.Config + expected zerolog.Level + }{ + { + name: "Debug level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: DebugLevel, + }, + expected: zerolog.DebugLevel, + }, + { + name: "Info level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: InfoLevel, + }, + expected: zerolog.InfoLevel, + }, + { + name: "Warn level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: WarnLevel, + }, + expected: zerolog.WarnLevel, + }, + { + name: "Error level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: ErrorLevel, + }, + expected: zerolog.ErrorLevel, + }, + { + name: "Fatal level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: FatalLevel, + }, + expected: zerolog.FatalLevel, + }, + { + name: "Panic level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: PanicLevel, + }, + expected: zerolog.PanicLevel, + }, + { + name: "Trace level", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: TraceLevel, + }, + expected: zerolog.TraceLevel, + }, + { + name: "Default level when empty", + cfg: &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "", + }, + expected: zerolog.InfoLevel, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger := NewLogger(tt.cfg) + + assert.Equal(t, tt.expected, logger.log.GetLevel()) + assert.NotNil(t, logger) + }) + } +} + +func Test_Logger_WithComponent(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "debug", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + componentLogger := logger.WithComponent("test-component") + componentLogger.Info().Msg("test message") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "test-component", logData["component"]) + assert.Equal(t, "test message", logData["message"]) + assert.Equal(t, "test-app", logData["service"]) + assert.Equal(t, "test", logData["environment"]) +} + +func Test_Logger_WithRequestId(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "debug", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + requestLogger := logger.WithRequestId("req-123") + requestLogger.Info().Msg("handling request") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "req-123", logData["request_id"]) +} + +func Test_Logger_WithTraceId(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "debug", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + traceLogger := logger.WithTraceId("trace-123") + traceLogger.Info().Msg("traced operation") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "trace-123", logData["trace_id"]) +} + +func Test_Logger_Debug(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "debug", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + logger.Debug().Msg("debug message") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "debug message", logData["message"]) +} + +func Test_Logger_Info(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "info", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + logger.Info().Msg("info message") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "info message", logData["message"]) +} + +func Test_Logger_Warn(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "warn", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + logger.Warn().Msg("warn message") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "warn message", logData["message"]) +} + +func Test_Logger_Error(t *testing.T) { + var buf bytes.Buffer + + cfg := &config.Config{ + AppName: "test-app", + AppEnv: "test", + LogLevel: "error", + } + + logger := NewLogger(cfg) + logger.log = logger.log.Output(&buf) + + logger.Error().Msg("error message") + + var logData map[string]interface{} + err := json.Unmarshal(buf.Bytes(), &logData) + + assert.NoError(t, err) + assert.Equal(t, "error message", logData["message"]) +} diff --git a/pkg/logger/module.go b/internal/config/logger/module.go similarity index 100% rename from pkg/logger/module.go rename to internal/config/logger/module.go diff --git a/internal/config/middlewares/authentication.go b/internal/config/middlewares/authentication.go index dea6eee..271980b 100644 --- a/internal/config/middlewares/authentication.go +++ b/internal/config/middlewares/authentication.go @@ -7,8 +7,8 @@ import ( "loki/internal/app/serializers" "loki/internal/app/services" + "loki/internal/config/logger" "loki/pkg/jwt" - "loki/pkg/logger" ) type AuthenticationMiddleware interface { diff --git a/internal/config/middlewares/authentication_test.go b/internal/config/middlewares/authentication_test.go index 9a92c79..d52101f 100644 --- a/internal/config/middlewares/authentication_test.go +++ b/internal/config/middlewares/authentication_test.go @@ -14,17 +14,24 @@ import ( "loki/internal/app/models" "loki/internal/app/serializers" "loki/internal/app/services" + "loki/internal/config" + "loki/internal/config/logger" "loki/pkg/jwt" - "loki/pkg/logger" ) -func Test_AuthMiddleware_Authenticate(t *testing.T) { +func Test_AuthenticationMiddleware_Authenticate(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + jwtService := jwt.NewMockJwt(ctrl) users := services.NewMockUsers(ctrl) - log := logger.NewLogger() middleware := NewAuthenticationMiddleware(jwtService, users, log) identityNumber := "PNOEE-123456789" diff --git a/internal/config/middlewares/authorization.go b/internal/config/middlewares/authorization.go index fbde388..32037d6 100644 --- a/internal/config/middlewares/authorization.go +++ b/internal/config/middlewares/authorization.go @@ -7,8 +7,8 @@ import ( "loki/internal/app/errors" "loki/internal/app/serializers" "loki/internal/app/services" + "loki/internal/config/logger" "loki/pkg/jwt" - "loki/pkg/logger" "loki/pkg/rbac" ) diff --git a/internal/config/middlewares/logger.go b/internal/config/middlewares/logger.go new file mode 100644 index 0000000..6bbd02f --- /dev/null +++ b/internal/config/middlewares/logger.go @@ -0,0 +1,61 @@ +package middlewares + +import ( + "net/http" + "time" + + "github.com/go-chi/chi/v5/middleware" + + "loki/internal/config/logger" +) + +type LoggerMiddleware interface { + Log(next http.Handler) http.Handler +} + +type loggerMiddleware struct { + log *logger.Logger +} + +func NewLoggerMiddleware(log *logger.Logger) LoggerMiddleware { + return &loggerMiddleware{ + log: log, + } +} + +func (m *loggerMiddleware) Log(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + traceId, _ := CurrentTraceIdFromContext(r.Context()) + requestId := middleware.GetReqID(r.Context()) + + reqLogger := m.log. + WithComponent("http"). + WithRequestId(requestId). + WithTraceId(traceId) + + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + next.ServeHTTP(ww, r) + + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + status := ww.Status() + size := ww.BytesWritten() + duration := time.Since(startTime) + + reqLogger.Info(). + Str("method", r.Method). + Str("uri", r.RequestURI). + Str("proto", r.Proto). + Str("scheme", scheme). + Str("remote_addr", r.RemoteAddr). + Str("user_agent", r.UserAgent()). + Int("status", status). + Int("size", size). + Dur("duration", duration). + Msgf("%s %s - %d %dB in %s", r.Method, r.RequestURI, status, size, duration) + }) +} diff --git a/internal/config/middlewares/logger_mock.go b/internal/config/middlewares/logger_mock.go new file mode 100644 index 0000000..567360b --- /dev/null +++ b/internal/config/middlewares/logger_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: internal/config/middlewares/logger.go +// +// Generated by this command: +// +// mockgen -source=internal/config/middlewares/logger.go -destination=internal/config/middlewares/logger_mock.go -package=middlewares +// + +// Package middlewares is a generated GoMock package. +package middlewares + +import ( + http "net/http" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockLoggerMiddleware is a mock of LoggerMiddleware interface. +type MockLoggerMiddleware struct { + ctrl *gomock.Controller + recorder *MockLoggerMiddlewareMockRecorder + isgomock struct{} +} + +// MockLoggerMiddlewareMockRecorder is the mock recorder for MockLoggerMiddleware. +type MockLoggerMiddlewareMockRecorder struct { + mock *MockLoggerMiddleware +} + +// NewMockLoggerMiddleware creates a new mock instance. +func NewMockLoggerMiddleware(ctrl *gomock.Controller) *MockLoggerMiddleware { + mock := &MockLoggerMiddleware{ctrl: ctrl} + mock.recorder = &MockLoggerMiddlewareMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLoggerMiddleware) EXPECT() *MockLoggerMiddlewareMockRecorder { + return m.recorder +} + +// Log mocks base method. +func (m *MockLoggerMiddleware) Log(next http.Handler) http.Handler { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Log", next) + ret0, _ := ret[0].(http.Handler) + return ret0 +} + +// Log indicates an expected call of Log. +func (mr *MockLoggerMiddlewareMockRecorder) Log(next any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLoggerMiddleware)(nil).Log), next) +} diff --git a/internal/config/middlewares/logger_test.go b/internal/config/middlewares/logger_test.go new file mode 100644 index 0000000..70e1880 --- /dev/null +++ b/internal/config/middlewares/logger_test.go @@ -0,0 +1,80 @@ +package middlewares + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + + "loki/internal/config" + "loki/internal/config/logger" +) + +func Test_NewLoggerMiddleware(t *testing.T) { + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + + middleware := NewLoggerMiddleware(log) + assert.NotNil(t, middleware) +} + +func Test_LoggerMiddleware_Log(t *testing.T) { + cfg := &config.Config{ + AppEnv: "test", + AppAddr: "localhost:8080", + LogLevel: "info", + } + log := logger.NewLogger(cfg) + middleware := NewLoggerMiddleware(log) + + type result struct { + code int + status string + } + + tests := []struct { + name string + traceId string + expected result + }{ + { + name: "Success", + traceId: "test-trace-id", + expected: result{ + code: http.StatusOK, + status: "200 OK", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("Success")) + }) + + req, err := http.NewRequest("GET", "/test", nil) + assert.NoError(t, err) + + req.Header.Set(TraceKey, tt.traceId) + + ctx := NewContextModifier(req.Context()). + WithTraceId(tt.traceId). + Context() + req = req.WithContext(ctx) + + rr := httptest.NewRecorder() + + middleware.Log(handler).ServeHTTP(rr, req) + + assert.Equal(t, tt.expected.code, rr.Code) + assert.Equal(t, tt.expected.status, rr.Result().Status) + }) + } +} diff --git a/internal/config/middlewares/modifier.go b/internal/config/middlewares/modifier.go index d8bb11d..0d20810 100644 --- a/internal/config/middlewares/modifier.go +++ b/internal/config/middlewares/modifier.go @@ -9,11 +9,13 @@ import ( type Claim struct{} type Token struct{} +type TraceId struct{} type CurrentUser struct{} type Modifier interface { WithClaim(claims *jwt.Payload) Modifier WithToken(token string) Modifier + WithTraceId(traceId string) Modifier WithCurrentUser(user *models.User) Modifier Context() context.Context } @@ -36,6 +38,11 @@ func (m *modifier) WithToken(token string) Modifier { return m } +func (m *modifier) WithTraceId(traceId string) Modifier { + m.ctx = context.WithValue(m.ctx, TraceId{}, traceId) + return m +} + func (m *modifier) WithCurrentUser(user *models.User) Modifier { m.ctx = context.WithValue(m.ctx, CurrentUser{}, user) return m diff --git a/internal/config/middlewares/modifier_test.go b/internal/config/middlewares/modifier_test.go index b8fc58a..d5b5f21 100644 --- a/internal/config/middlewares/modifier_test.go +++ b/internal/config/middlewares/modifier_test.go @@ -92,6 +92,34 @@ func Test_Modifier_WithToken(t *testing.T) { } } +func Test_Modifier_WithTraceId(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + traceId string + }{ + { + name: "Valid trace ID", + traceId: "valid-trace-id", + }, + { + name: "Empty trace ID", + traceId: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctxModifier := NewContextModifier(ctx).WithTraceId(tt.traceId) + + traceId, ok := ctxModifier.Context().Value(TraceId{}).(string) + assert.True(t, ok) + assert.Equal(t, tt.traceId, traceId) + }) + } +} + func Test_Modifier_WithCurrentUser(t *testing.T) { ctx := context.Background() diff --git a/internal/config/middlewares/module.go b/internal/config/middlewares/module.go index 5e90b35..f623f3f 100644 --- a/internal/config/middlewares/module.go +++ b/internal/config/middlewares/module.go @@ -5,6 +5,6 @@ import "go.uber.org/fx" var Module = fx.Options( fx.Provide(NewAuthenticationMiddleware), fx.Provide(NewAuthorizationMiddleware), - fx.Provide(NewPaginationMiddleware), fx.Provide(NewTelemetryMiddleware), + fx.Provide(NewLoggerMiddleware), ) diff --git a/internal/config/middlewares/pagination.go b/internal/config/middlewares/pagination.go deleted file mode 100644 index 4bf2496..0000000 --- a/internal/config/middlewares/pagination.go +++ /dev/null @@ -1,47 +0,0 @@ -package middlewares - -import ( - "context" - "net/http" - - "loki/internal/app/services" - "loki/pkg/logger" -) - -type PaginationKey struct{} - -type PaginationMiddleware interface { - Paginate(next http.Handler) http.Handler -} - -type paginationMiddleware struct { - pagination services.Pagination - log *logger.Logger -} - -func NewPaginationMiddleware(pagination services.Pagination, log *logger.Logger) PaginationMiddleware { - return &paginationMiddleware{ - pagination: pagination, - log: log, - } -} - -func (p *paginationMiddleware) Paginate(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := withPagination(r.Context(), p.pagination) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -func withPagination(ctx context.Context, pagination services.Pagination) context.Context { - return context.WithValue(ctx, PaginationKey{}, pagination) -} - -func CurrentPaginationFromContext(ctx context.Context) *services.Pagination { - pagination, ok := ctx.Value(PaginationKey{}).(*services.Pagination) - if !ok { - return services.NewPagination(nil) - } - - return pagination -} diff --git a/internal/config/middlewares/telemetry.go b/internal/config/middlewares/telemetry.go index a25f6eb..31f473d 100644 --- a/internal/config/middlewares/telemetry.go +++ b/internal/config/middlewares/telemetry.go @@ -11,7 +11,7 @@ import ( ) const ( - AuthenticationTraceKey = "X-Trace-ID" + TraceKey = "X-Trace-ID" AuthenticationTraceName = "authentication" ) @@ -28,20 +28,24 @@ func NewTelemetryMiddleware() TelemetryMiddleware { func (m *telemetryMiddleware) Trace(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tracer := otel.Tracer(AuthenticationTraceName) - traceId := r.Header.Get(AuthenticationTraceKey) + traceId := r.Header.Get(TraceKey) if traceId == "" { traceId = uuid.New().String() - r.Header.Set(AuthenticationTraceKey, traceId) + r.Header.Set(TraceKey, traceId) } + ctx := NewContextModifier(r.Context()). + WithTraceId(traceId). + Context() + id, _ := trace.TraceIDFromHex(formatToTraceID(traceId)) spanCtx := trace.NewSpanContext(trace.SpanContextConfig{ TraceID: id, Remote: true, }) - ctx := trace.ContextWithSpanContext(r.Context(), spanCtx) + ctx = trace.ContextWithSpanContext(ctx, spanCtx) ctx, span := tracer.Start(ctx, formatToOperationName(r.URL.Path)) defer span.End() diff --git a/internal/config/middlewares/utils.go b/internal/config/middlewares/utils.go index 4bfe840..6d20eac 100644 --- a/internal/config/middlewares/utils.go +++ b/internal/config/middlewares/utils.go @@ -26,3 +26,8 @@ func CurrentClaimFromContext(ctx context.Context) (*jwt.Payload, bool) { c, ok := ctx.Value(Claim{}).(*jwt.Payload) return c, ok } + +func CurrentTraceIdFromContext(ctx context.Context) (string, bool) { + t, ok := ctx.Value(TraceId{}).(string) + return t, ok +} diff --git a/internal/config/middlewares/utils_test.go b/internal/config/middlewares/utils_test.go index bf82344..75941b2 100644 --- a/internal/config/middlewares/utils_test.go +++ b/internal/config/middlewares/utils_test.go @@ -103,3 +103,38 @@ func Test_CurrentClaimFromContext(t *testing.T) { }) } } + +func Test_CurrentTraceIdFromContext(t *testing.T) { + tests := []struct { + name string + ctx context.Context + traceId string + exists bool + }{ + { + name: "Success", + ctx: context.WithValue(context.Background(), TraceId{}, "9809b3e0-484b-438c-80b2-73cb9af51cd4"), + traceId: "9809b3e0-484b-438c-80b2-73cb9af51cd4", + exists: true, + }, + { + name: "TraceId does not exist", + ctx: context.Background(), + traceId: "", + exists: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + traceId, exists := CurrentTraceIdFromContext(tt.ctx) + assert.Equal(t, tt.exists, exists) + + if tt.exists { + assert.Equal(t, tt.traceId, traceId) + } else { + assert.Empty(t, traceId) + } + }) + } +} diff --git a/internal/config/router/router.go b/internal/config/router/router.go index 9b70fb0..ffdff36 100644 --- a/internal/config/router/router.go +++ b/internal/config/router/router.go @@ -20,6 +20,7 @@ func NewRouter( authentication middlewares.AuthenticationMiddleware, authorization middlewares.AuthorizationMiddleware, telemetry middlewares.TelemetryMiddleware, + logger middlewares.LoggerMiddleware, health controllers.HealthController, smartId controllers.SmartIdController, @@ -38,7 +39,7 @@ func NewRouter( r.Use(telemetry.Trace) r.Use(middleware.RequestID) - r.Use(middleware.Logger) + r.Use(logger.Log) r.Use(middleware.Compress(5)) r.Use(middleware.Heartbeat("/health")) r.Use( diff --git a/internal/config/router/router_test.go b/internal/config/router/router_test.go index aa58137..3d91eb2 100644 --- a/internal/config/router/router_test.go +++ b/internal/config/router/router_test.go @@ -26,6 +26,7 @@ func Test_HealthCheck(t *testing.T) { mockAuthenticationMiddleware := middlewares.NewMockAuthenticationMiddleware(ctrl) mockAuthorizationMiddleware := middlewares.NewMockAuthorizationMiddleware(ctrl) mockTelemetryMiddleware := middlewares.NewMockTelemetryMiddleware(ctrl) + mockLoggerMiddleware := middlewares.NewMockLoggerMiddleware(ctrl) mockHealthController := controllers.NewMockHealthController(ctrl) mockSmartIdController := controllers.NewMockSmartIdController(ctrl) @@ -66,12 +67,19 @@ func Test_HealthCheck(t *testing.T) { DoAndReturn(func(next http.Handler) http.Handler { return next }) + mockLoggerMiddleware.EXPECT(). + Log(gomock.Any()). + AnyTimes(). + DoAndReturn(func(next http.Handler) http.Handler { + return next + }) router := NewRouter( cfg, mockAuthenticationMiddleware, mockAuthorizationMiddleware, mockTelemetryMiddleware, + mockLoggerMiddleware, mockHealthController, mockSmartIdController, mockMobileIdController, diff --git a/internal/config/server/grpc.go b/internal/config/server/grpc.go index fd266f7..3a43de2 100644 --- a/internal/config/server/grpc.go +++ b/internal/config/server/grpc.go @@ -18,7 +18,7 @@ import ( "loki/internal/app/rpcs" "loki/internal/app/rpcs/interceptors" "loki/internal/config" - "loki/pkg/logger" + "loki/internal/config/logger" ) const ( @@ -47,8 +47,10 @@ type grpcServer struct { func NewGrpcServer( cfg *config.Config, - authInterceptor interceptors.AuthenticationInterceptor, registry *rpcs.Registry, + authenticationInterceptor interceptors.AuthenticationInterceptor, + traceInterceptor interceptors.TraceInterceptor, + loggerInterceptor interceptors.LoggerInterceptor, log *logger.Logger, ) GrpcServer { tlsConfig, err := setupTLS(cfg, log) @@ -64,15 +66,16 @@ func NewGrpcServer( Timeout: KeepaliveTimeout, } + unaryInterceptors := []grpc.UnaryServerInterceptor{ + traceInterceptor.Trace(), + loggerInterceptor.Log(), + auth.UnaryServerInterceptor(authenticationInterceptor.Authenticate), + } + server := grpc.NewServer( grpc.Creds(credentials.NewTLS(tlsConfig)), grpc.KeepaliveParams(options), - grpc.UnaryInterceptor( - auth.UnaryServerInterceptor(authInterceptor.Authenticate), - ), - grpc.StreamInterceptor( - auth.StreamServerInterceptor(authInterceptor.Authenticate), - ), + grpc.ChainUnaryInterceptor(unaryInterceptors...), grpc.StatsHandler(otelgrpc.NewServerHandler()), ) registry.RegisterAll(server) diff --git a/internal/config/server/grpc_test.go b/internal/config/server/grpc_test.go index 297ac27..d222237 100644 --- a/internal/config/server/grpc_test.go +++ b/internal/config/server/grpc_test.go @@ -20,7 +20,7 @@ import ( "loki/internal/app/rpcs" "loki/internal/app/rpcs/interceptors" "loki/internal/config" - "loki/pkg/logger" + "loki/internal/config/logger" ) func Test_NewGrpcServer(t *testing.T) { @@ -31,14 +31,24 @@ func Test_NewGrpcServer(t *testing.T) { cfg := &config.Config{ AppEnv: "test", + AppAddr: "localhost:8080", GrpcAddr: "localhost:50051", CertPath: certDir, + LogLevel: "info", } + log := logger.NewLogger(cfg) + authInterceptor := interceptors.NewMockAuthenticationInterceptor(ctrl) + traceInterceptor := interceptors.NewMockTraceInterceptor(ctrl) + loggerInterceptor := interceptors.NewMockLoggerInterceptor(ctrl) + + authInterceptor.EXPECT().Authenticate(gomock.Any()).AnyTimes() + traceInterceptor.EXPECT().Trace().AnyTimes() + loggerInterceptor.EXPECT().Log().AnyTimes() + registry := &rpcs.Registry{} - log := logger.NewLogger() - srv := NewGrpcServer(cfg, authInterceptor, registry, log) + srv := NewGrpcServer(cfg, registry, authInterceptor, traceInterceptor, loggerInterceptor, log) assert.NotNil(t, srv) s, ok := srv.(*grpcServer) @@ -55,14 +65,24 @@ func Test_GrpcServer_RunAndShutdown(t *testing.T) { cfg := &config.Config{ AppEnv: "test", + AppAddr: "localhost:8080", GrpcAddr: "localhost:50051", CertPath: certDir, + LogLevel: "info", } + log := logger.NewLogger(cfg) + authInterceptor := interceptors.NewMockAuthenticationInterceptor(ctrl) + traceInterceptor := interceptors.NewMockTraceInterceptor(ctrl) + loggerInterceptor := interceptors.NewMockLoggerInterceptor(ctrl) + + authInterceptor.EXPECT().Authenticate(gomock.Any()).AnyTimes() + traceInterceptor.EXPECT().Trace().AnyTimes() + loggerInterceptor.EXPECT().Log().AnyTimes() + registry := &rpcs.Registry{} - log := logger.NewLogger() - srv := NewGrpcServer(cfg, authInterceptor, registry, log) + srv := NewGrpcServer(cfg, registry, authInterceptor, traceInterceptor, loggerInterceptor, log) assert.NotNil(t, srv) runErrCh := make(chan error, 1) diff --git a/internal/config/server/server_test.go b/internal/config/server/server_test.go index b0817bd..8bfb6f1 100644 --- a/internal/config/server/server_test.go +++ b/internal/config/server/server_test.go @@ -28,6 +28,7 @@ func Test_NewWebServer(t *testing.T) { mockAuthenticationMiddleware := middlewares.NewMockAuthenticationMiddleware(ctrl) mockAuthorizationMiddleware := middlewares.NewMockAuthorizationMiddleware(ctrl) mockTelemetryMiddleware := middlewares.NewMockTelemetryMiddleware(ctrl) + mockLoggerMiddleware := middlewares.NewMockLoggerMiddleware(ctrl) mockHealthController := controllers.NewMockHealthController(ctrl) mockSmartIdController := controllers.NewMockSmartIdController(ctrl) @@ -68,12 +69,19 @@ func Test_NewWebServer(t *testing.T) { DoAndReturn(func(next http.Handler) http.Handler { return next }) + mockLoggerMiddleware.EXPECT(). + Log(gomock.Any()). + AnyTimes(). + DoAndReturn(func(next http.Handler) http.Handler { + return next + }) appRouter := router.NewRouter( cfg, mockAuthenticationMiddleware, mockAuthorizationMiddleware, mockTelemetryMiddleware, + mockLoggerMiddleware, mockHealthController, mockSmartIdController, mockMobileIdController, diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go deleted file mode 100644 index 6ba3862..0000000 --- a/pkg/logger/logger.go +++ /dev/null @@ -1,72 +0,0 @@ -package logger - -import ( - "io" - "os" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/pkgerrors" -) - -type Logger struct { - log zerolog.Logger -} - -func NewLogger() *Logger { - zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack - zerolog.TimeFieldFormat = "2006-01-02 15:04:05" - - var output io.Writer = zerolog.ConsoleWriter{ - Out: os.Stdout, - TimeFormat: zerolog.TimeFieldFormat, - } - - log := zerolog.New(output). - Level(getLogLevel()). - With(). - Timestamp(). - Logger() - - return &Logger{log: log} -} - -func (l *Logger) Debug() *zerolog.Event { - return l.log.Debug() -} - -func (l *Logger) Info() *zerolog.Event { - return l.log.Info() -} - -func (l *Logger) Warn() *zerolog.Event { - return l.log.Warn() -} - -func (l *Logger) Error() *zerolog.Event { - return l.log.Error() -} - -func getLogLevel() zerolog.Level { - if envValue, ok := os.LookupEnv("LOG_LEVEL"); ok { - switch envValue { - case "debug": - return zerolog.DebugLevel - case "info": - return zerolog.InfoLevel - case "warn": - return zerolog.WarnLevel - case "error": - return zerolog.ErrorLevel - case "fatal": - return zerolog.FatalLevel - case "panic": - return zerolog.PanicLevel - case "trace": - return zerolog.TraceLevel - default: - return zerolog.InfoLevel - } - } - - return zerolog.InfoLevel -} diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go deleted file mode 100644 index 10fac62..0000000 --- a/pkg/logger/logger_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package logger - -import ( - "bytes" - "os" - "testing" - - "github.com/rs/zerolog" - "github.com/stretchr/testify/assert" -) - -func Test_NewLogger(t *testing.T) { - tests := []struct { - name string - expected zerolog.Level - }{ - { - name: "Default configuration", - expected: zerolog.InfoLevel, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - logger := NewLogger() - - assert.Equal(t, tt.expected, logger.log.GetLevel()) - assert.NotNil(t, logger) - }) - } -} - -func Test_Logger_Debug(t *testing.T) { - tests := []struct { - name string - expected string - }{ - { - name: "Success", - expected: "debug", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - logger := NewLogger() - logger.log = logger.log.Output(&buf) - - logger.Info().Msg(tt.expected) - - result := buf.String() - - assert.Contains(t, result, tt.expected) - }) - } -} - -func Test_Logger_Info(t *testing.T) { - tests := []struct { - name string - expected string - }{ - { - name: "Success", - expected: "info", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - logger := NewLogger() - logger.log = logger.log.Output(&buf) - - logger.Info().Msg(tt.expected) - - result := buf.String() - - assert.Contains(t, result, tt.expected) - }) - } -} - -func Test_Logger_Warn(t *testing.T) { - tests := []struct { - name string - expected string - }{ - { - name: "Success", - expected: "warn", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - logger := NewLogger() - logger.log = logger.log.Output(&buf) - - logger.Warn().Msg(tt.expected) - - result := buf.String() - - assert.Contains(t, result, tt.expected) - }) - } -} - -func Test_Logger_Error(t *testing.T) { - tests := []struct { - name string - expected string - }{ - { - name: "Success", - expected: "error", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - logger := NewLogger() - logger.log = logger.log.Output(&buf) - - logger.Error().Msg(tt.expected) - - result := buf.String() - - assert.Contains(t, result, tt.expected) - }) - } -} - -func Test_getLogLevel(t *testing.T) { - tests := []struct { - name string - before func() - expected zerolog.Level - }{ - { - name: "Debug level", - before: func() { - err := os.Setenv("LOG_LEVEL", "debug") - assert.NoError(t, err) - }, - expected: zerolog.DebugLevel, - }, - { - name: "Info level", - before: func() { - err := os.Setenv("LOG_LEVEL", "info") - assert.NoError(t, err) - }, - expected: zerolog.InfoLevel, - }, - { - name: "Warn level", - before: func() { - err := os.Setenv("LOG_LEVEL", "warn") - assert.NoError(t, err) - }, - expected: zerolog.WarnLevel, - }, - { - name: "Error level", - before: func() { - err := os.Setenv("LOG_LEVEL", "error") - assert.NoError(t, err) - }, - expected: zerolog.ErrorLevel, - }, - { - name: "Fatal level", - before: func() { - err := os.Setenv("LOG_LEVEL", "fatal") - assert.NoError(t, err) - }, - expected: zerolog.FatalLevel, - }, - { - name: "Panic level", - before: func() { - err := os.Setenv("LOG_LEVEL", "panic") - assert.NoError(t, err) - }, - expected: zerolog.PanicLevel, - }, - { - name: "Trace level", - before: func() { - err := os.Setenv("LOG_LEVEL", "trace") - assert.NoError(t, err) - }, - expected: zerolog.TraceLevel, - }, - { - name: "Default level", - before: func() {}, - expected: zerolog.InfoLevel, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - level := getLogLevel() - - assert.Equal(t, tt.expected, level) - - t.Cleanup(func() { - err := os.Unsetenv("LOG_LEVEL") - assert.NoError(t, err) - }) - }) - } -} From 3c98844c8355e7013843634adcf143c6a03461a6 Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 8 Apr 2025 21:30:18 +0300 Subject: [PATCH 13/20] chore(env): Add gRPC address to .env.development --- .env.development | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.development b/.env.development index 272305d..d9d1531 100644 --- a/.env.development +++ b/.env.development @@ -3,6 +3,7 @@ LOG_LEVEL=info APP_NAME=loki APP_ADDRESS=0.0.0.0:8080 CLIENT_URL=http://localhost:8080 +GRPC_ADDRESS=0.0.0.0:50051 DATABASE_DSN=postgres://postgres:postgres@localhost:5432/loki-development?sslmode=disable From 76c0c4ba43f687255276774cf9e046f200f8ed9e Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 8 Apr 2025 21:30:47 +0300 Subject: [PATCH 14/20] docs(certificates): Add documentation for generating JWT signing keys and mTLS certificates --- docs/certificates.md | 127 +++++++++++++++++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 128 insertions(+) create mode 100644 docs/certificates.md diff --git a/docs/certificates.md b/docs/certificates.md new file mode 100644 index 0000000..3ca582b --- /dev/null +++ b/docs/certificates.md @@ -0,0 +1,127 @@ +# Certificates and keys + +## Generating JWT Signing Key pair + +Loki uses JWT (JSON Web Tokens) for authentication. + +To generate a signing key for JWT, you can use the following command: + +```sh +openssl genrsa -out certs/jwt/private.key 4096 +``` + +To generate the public key from the private key, use: + +```sh +openssl rsa -in certs/jwt/private.key -pubout -out certs/jwt/public.key +``` + +## Generating Certificates for mTLS + +For mTLS (mutual TLS), both the server and client need certificates. +The process involves: + +- Creating a Certificate Authority (CA) +- Creating server certificates signed by the CA +- Creating client certificates signed by the CA + +### Generate the Certificate Authority (CA) + +Generate a private key for your CA + +```sh +openssl genrsa -out certs/ca.key 4096 +openssl req -new -x509 -key certs/ca.key -sha256 -subj "/CN=Loki CA" -out certs/ca.pem -days 3650 +``` + +### Generate the Server Certificate + +#### Generate server private key + +```sh +openssl genrsa -out certs/server.key 4096 +``` + +#### Create server Certificate Signing Request (CSR) + +```sh +openssl req -new -key certs/server.key -out certs/server.csr -config <( +cat <<-EOF +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +req_extensions = req_ext +distinguished_name = dn + +[dn] +CN = loki-backend + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = backend +IP.1 = 127.0.0.1 +IP.2 = 0.0.0.0 +EOF +) +``` + +#### Sign the server certificate with CA + +```sh +openssl x509 -req -in certs/server.csr -CA certs/ca.pem -CAkey certs/ca.key -CAcreateserial -out certs/server.pem -days 825 -sha256 -extfile <( +cat <<-EOF +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = backend +IP.1 = 127.0.0.1 +IP.2 = 0.0.0.0 +EOF +) +``` + +### Generate the Client Certificate + +Generate client private key + +```sh +openssl genrsa -out certs/client.key 4096 +``` + +#### Create client Certificate Signing Request (CSR) + +```sh +openssl req -new -key certs/client.key -out certs/client.csr -config <( +cat <<-EOF +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +distinguished_name = dn + +[dn] +CN = loki-backoffice +EOF +) +``` + +#### Sign the client certificate with CA + +```sh +openssl x509 -req -in certs/client.csr -CA certs/ca.pem -CAkey certs/ca.key -CAcreateserial -out certs/client.pem -days 825 -sha256 +``` + +### Verify the certificates + +```sh +openssl verify -CAfile certs/ca.pem certs/server.pem +``` + +```sh +openssl verify -CAfile certs/ca.pem certs/client.pem +``` diff --git a/docs/index.md b/docs/index.md index 2992307..0740ae8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,4 +14,5 @@ Designed to be easily integrated into microservices architectures and provides l ## Contents - [Installation](installation.md) +- [Certificates](certificates.md) - [Usage](usage.md) From 468f8d69fa908650a7764fe2f1629e56020f647c Mon Sep 17 00:00:00 2001 From: tab Date: Tue, 8 Apr 2025 21:56:12 +0300 Subject: [PATCH 15/20] refactor(backoffice): Remove backoffice controllers Deleted the backoffice controllers and tests REST API replaced with gRPC --- internal/app/app.go | 2 - internal/app/controllers/backoffice/module.go | 11 - .../app/controllers/backoffice/permissions.go | 177 ------ .../backoffice/permissions_mock.go | 101 ---- .../backoffice/permissions_test.go | 532 ---------------- internal/app/controllers/backoffice/roles.go | 181 ------ .../app/controllers/backoffice/roles_mock.go | 101 ---- .../app/controllers/backoffice/roles_test.go | 571 ------------------ internal/app/controllers/backoffice/scopes.go | 177 ------ .../app/controllers/backoffice/scopes_mock.go | 101 ---- .../app/controllers/backoffice/scopes_test.go | 532 ---------------- internal/app/controllers/backoffice/tokens.go | 80 --- .../app/controllers/backoffice/tokens_mock.go | 65 -- .../app/controllers/backoffice/tokens_test.go | 218 ------- internal/app/controllers/backoffice/users.go | 193 ------ .../app/controllers/backoffice/users_mock.go | 101 ---- .../app/controllers/backoffice/users_test.go | 560 ----------------- internal/config/router/router.go | 42 -- internal/config/router/router_test.go | 28 - internal/config/server/server_test.go | 28 - 20 files changed, 3801 deletions(-) delete mode 100644 internal/app/controllers/backoffice/module.go delete mode 100644 internal/app/controllers/backoffice/permissions.go delete mode 100644 internal/app/controllers/backoffice/permissions_mock.go delete mode 100644 internal/app/controllers/backoffice/permissions_test.go delete mode 100644 internal/app/controllers/backoffice/roles.go delete mode 100644 internal/app/controllers/backoffice/roles_mock.go delete mode 100644 internal/app/controllers/backoffice/roles_test.go delete mode 100644 internal/app/controllers/backoffice/scopes.go delete mode 100644 internal/app/controllers/backoffice/scopes_mock.go delete mode 100644 internal/app/controllers/backoffice/scopes_test.go delete mode 100644 internal/app/controllers/backoffice/tokens.go delete mode 100644 internal/app/controllers/backoffice/tokens_mock.go delete mode 100644 internal/app/controllers/backoffice/tokens_test.go delete mode 100644 internal/app/controllers/backoffice/users.go delete mode 100644 internal/app/controllers/backoffice/users_mock.go delete mode 100644 internal/app/controllers/backoffice/users_test.go diff --git a/internal/app/app.go b/internal/app/app.go index b13049a..302a7c6 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -10,7 +10,6 @@ import ( "go.uber.org/fx" "loki/internal/app/controllers" - "loki/internal/app/controllers/backoffice" "loki/internal/app/repositories" "loki/internal/app/rpcs" "loki/internal/app/services" @@ -30,7 +29,6 @@ var Module = fx.Options( authentication.Module, controllers.Module, - backoffice.Module, repositories.Module, jwt.Module, services.Module, diff --git a/internal/app/controllers/backoffice/module.go b/internal/app/controllers/backoffice/module.go deleted file mode 100644 index 208a8d0..0000000 --- a/internal/app/controllers/backoffice/module.go +++ /dev/null @@ -1,11 +0,0 @@ -package backoffice - -import "go.uber.org/fx" - -var Module = fx.Options( - fx.Provide(NewPermissionsController), - fx.Provide(NewRolesController), - fx.Provide(NewScopesController), - fx.Provide(NewTokensController), - fx.Provide(NewUsersController), -) diff --git a/internal/app/controllers/backoffice/permissions.go b/internal/app/controllers/backoffice/permissions.go deleted file mode 100644 index 71e48ca..0000000 --- a/internal/app/controllers/backoffice/permissions.go +++ /dev/null @@ -1,177 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/models/dto" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -type PermissionsController interface { - List(w http.ResponseWriter, r *http.Request) - Get(w http.ResponseWriter, r *http.Request) - Create(w http.ResponseWriter, r *http.Request) - Update(w http.ResponseWriter, r *http.Request) - Delete(w http.ResponseWriter, r *http.Request) -} - -type permissionsController struct { - permissions services.Permissions -} - -func NewPermissionsController(permissions services.Permissions) PermissionsController { - return &permissionsController{ - permissions: permissions, - } -} - -//nolint:dupl -func (c *permissionsController) List(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - pagination := services.NewPagination(r) - rows, total, err := c.permissions.List(r.Context(), pagination) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - collection := make([]serializers.PermissionSerializer, 0, len(rows)) - - for _, row := range rows { - collection = append(collection, serializers.PermissionSerializer{ - ID: row.ID, - Name: row.Name, - Description: row.Description, - }) - } - - response := serializers.PaginationResponse[serializers.PermissionSerializer]{ - Data: collection, - Meta: serializers.PaginationMeta{ - Page: pagination.Page, - Per: pagination.PerPage, - Total: total, - }, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *permissionsController) Get(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - record, err := c.permissions.FindById(r.Context(), id) - if err != nil { - if errors.Is(err, errors.ErrPermissionNotFound) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.PermissionSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *permissionsController) Create(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - var params dto.PermissionRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.permissions.Create(r.Context(), &models.Permission{ - Name: params.Name, - Description: params.Description, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.PermissionSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *permissionsController) Update(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - var params dto.PermissionRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.permissions.Update(r.Context(), &models.Permission{ - ID: id, - Name: params.Name, - Description: params.Description, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.PermissionSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *permissionsController) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - _, err := c.permissions.Delete(r.Context(), id) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/app/controllers/backoffice/permissions_mock.go b/internal/app/controllers/backoffice/permissions_mock.go deleted file mode 100644 index 56402b0..0000000 --- a/internal/app/controllers/backoffice/permissions_mock.go +++ /dev/null @@ -1,101 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: internal/app/controllers/backoffice/permissions.go -// -// Generated by this command: -// -// mockgen -source=internal/app/controllers/backoffice/permissions.go -destination=internal/app/controllers/backoffice/permissions_mock.go -package=backoffice -// - -// Package backoffice is a generated GoMock package. -package backoffice - -import ( - http "net/http" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockPermissionsController is a mock of PermissionsController interface. -type MockPermissionsController struct { - ctrl *gomock.Controller - recorder *MockPermissionsControllerMockRecorder - isgomock struct{} -} - -// MockPermissionsControllerMockRecorder is the mock recorder for MockPermissionsController. -type MockPermissionsControllerMockRecorder struct { - mock *MockPermissionsController -} - -// NewMockPermissionsController creates a new mock instance. -func NewMockBackofficePermissionsController(ctrl *gomock.Controller) *MockPermissionsController { - mock := &MockPermissionsController{ctrl: ctrl} - mock.recorder = &MockPermissionsControllerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockPermissionsController) EXPECT() *MockPermissionsControllerMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *MockPermissionsController) Create(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Create", w, r) -} - -// Create indicates an expected call of Create. -func (mr *MockPermissionsControllerMockRecorder) Create(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockPermissionsController)(nil).Create), w, r) -} - -// Delete mocks base method. -func (m *MockPermissionsController) Delete(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", w, r) -} - -// Delete indicates an expected call of Delete. -func (mr *MockPermissionsControllerMockRecorder) Delete(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockPermissionsController)(nil).Delete), w, r) -} - -// Get mocks base method. -func (m *MockPermissionsController) Get(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Get", w, r) -} - -// Get indicates an expected call of Get. -func (mr *MockPermissionsControllerMockRecorder) Get(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPermissionsController)(nil).Get), w, r) -} - -// List mocks base method. -func (m *MockPermissionsController) List(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "List", w, r) -} - -// List indicates an expected call of List. -func (mr *MockPermissionsControllerMockRecorder) List(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPermissionsController)(nil).List), w, r) -} - -// Update mocks base method. -func (m *MockPermissionsController) Update(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Update", w, r) -} - -// Update indicates an expected call of Update. -func (mr *MockPermissionsControllerMockRecorder) Update(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockPermissionsController)(nil).Update), w, r) -} diff --git a/internal/app/controllers/backoffice/permissions_test.go b/internal/app/controllers/backoffice/permissions_test.go deleted file mode 100644 index 747189e..0000000 --- a/internal/app/controllers/backoffice/permissions_test.go +++ /dev/null @@ -1,532 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -func Test_Backoffice_Permissions_List(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - permissions := services.NewMockPermissions(ctrl) - controller := NewPermissionsController(permissions) - - type result struct { - response serializers.PaginationResponse[serializers.PermissionSerializer] - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - permissions.EXPECT().List(gomock.Any(), gomock.Any()).Return([]models.Permission{ - { - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, - { - ID: uuid.MustParse("10000000-1000-1000-3000-000000000002"), - Name: "write:self", - Description: "Write own data", - }, - }, uint64(2), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.PermissionSerializer]{ - Data: []serializers.PermissionSerializer{ - { - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, - { - ID: uuid.MustParse("10000000-1000-1000-3000-000000000002"), - Name: "write:self", - Description: "Write own data", - }, - }, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: uint64(2), - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Empty", - before: func() { - permissions.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.PermissionSerializer]{ - Data: []serializers.PermissionSerializer{}, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: uint64(0), - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Error", - before: func() { - permissions.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/permissions", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/permissions", controller.List) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PaginationResponse[serializers.PermissionSerializer] - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Permissions_Get(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - permissions := services.NewMockPermissions(ctrl) - controller := NewPermissionsController(permissions) - - type result struct { - response serializers.PermissionSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - permissions.EXPECT().FindById(gomock.Any(), uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(&models.Permission{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, nil) - }, - expected: result{ - response: serializers.PermissionSerializer{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Not found", - before: func() { - permissions.EXPECT().FindById(gomock.Any(), uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(&models.Permission{}, errors.ErrPermissionNotFound) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: errors.ErrPermissionNotFound.Error()}, - status: "404 Not Found", - code: http.StatusNotFound, - }, - }, - { - name: "Error", - before: func() { - permissions.EXPECT().FindById(gomock.Any(), uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(&models.Permission{}, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/permissions/10000000-1000-1000-3000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/permissions/{id}", controller.Get) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PermissionSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Permissions_Create(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - permissions := services.NewMockPermissions(ctrl) - controller := NewPermissionsController(permissions) - - type result struct { - response serializers.PermissionSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - permissions.EXPECT().Create(gomock.Any(), &models.Permission{ - Name: "read:self", - Description: "Read own data", - }).Return(&models.Permission{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, nil) - }, - body: strings.NewReader(`{"name": "read:self", "description": "Read own data"}`), - expected: result{ - response: serializers.PermissionSerializer{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, - status: "201 Created", - code: http.StatusCreated, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - permissions.EXPECT().Create(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"name": "read:self"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty description"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - permissions.EXPECT().Create(gomock.Any(), &models.Permission{ - Name: "read:self", - Description: "Read own data", - }).Return(&models.Permission{}, assert.AnError) - }, - body: strings.NewReader(`{"name": "read:self", "description": "Read own data"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPost, "/api/backoffice/permissions", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Post("/api/backoffice/permissions", controller.Create) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PermissionSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Permissions_Update(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - permissions := services.NewMockPermissions(ctrl) - controller := NewPermissionsController(permissions) - - type result struct { - response serializers.PermissionSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - permissions.EXPECT().Update(gomock.Any(), &models.Permission{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }).Return(&models.Permission{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, nil) - }, - body: strings.NewReader(`{"name": "read:self", "description": "Read own data"}`), - expected: result{ - response: serializers.PermissionSerializer{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - permissions.EXPECT().Update(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"name": "read:self"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty description"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - permissions.EXPECT().Update(gomock.Any(), &models.Permission{ - ID: uuid.MustParse("10000000-1000-1000-3000-000000000001"), - Name: "read:self", - Description: "Read own data", - }).Return(&models.Permission{}, assert.AnError) - }, - body: strings.NewReader(`{"name": "read:self", "description": "Read own data"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPut, "/api/backoffice/permissions/10000000-1000-1000-3000-000000000001", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Put("/api/backoffice/permissions/{id}", controller.Update) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PermissionSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Permissions_Delete(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - permissions := services.NewMockPermissions(ctrl) - controller := NewPermissionsController(permissions) - - type result struct { - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - permissions.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(true, nil) - }, - expected: result{ - status: "204 No Content", - code: http.StatusNoContent, - }, - error: false, - }, - { - name: "Error", - before: func() { - permissions.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-3000-000000000001")).Return(false, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodDelete, "/api/backoffice/permissions/10000000-1000-1000-3000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Delete("/api/backoffice/permissions/{id}", controller.Delete) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} diff --git a/internal/app/controllers/backoffice/roles.go b/internal/app/controllers/backoffice/roles.go deleted file mode 100644 index 2f08219..0000000 --- a/internal/app/controllers/backoffice/roles.go +++ /dev/null @@ -1,181 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "net/http" - - "loki/internal/app/models/dto" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -type RolesController interface { - List(w http.ResponseWriter, r *http.Request) - Get(w http.ResponseWriter, r *http.Request) - Create(w http.ResponseWriter, r *http.Request) - Update(w http.ResponseWriter, r *http.Request) - Delete(w http.ResponseWriter, r *http.Request) -} - -type rolesController struct { - roles services.Roles -} - -func NewRolesController(roles services.Roles) RolesController { - return &rolesController{ - roles: roles, - } -} - -//nolint:dupl -func (c *rolesController) List(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - paginator := services.NewPagination(r) - rows, total, err := c.roles.List(r.Context(), paginator) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - collection := make([]serializers.RoleSerializer, 0, len(rows)) - - for _, row := range rows { - collection = append(collection, serializers.RoleSerializer{ - ID: row.ID, - Name: row.Name, - Description: row.Description, - }) - } - - response := serializers.PaginationResponse[serializers.RoleSerializer]{ - Data: collection, - Meta: serializers.PaginationMeta{ - Page: paginator.Page, - Per: paginator.PerPage, - Total: total, - }, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *rolesController) Get(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - record, err := c.roles.FindRoleDetailsById(r.Context(), id) - if err != nil { - if errors.Is(err, errors.ErrRoleNotFound) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.RoleSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - PermissionIDs: record.PermissionIDs, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *rolesController) Create(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - var params dto.RoleRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.roles.Create(r.Context(), &models.Role{ - Name: params.Name, - Description: params.Description, - PermissionIDs: params.PermissionIDs, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.RoleSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *rolesController) Update(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - var params dto.RoleRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.roles.Update(r.Context(), &models.Role{ - ID: id, - Name: params.Name, - Description: params.Description, - PermissionIDs: params.PermissionIDs, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.RoleSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *rolesController) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - _, err := c.roles.Delete(r.Context(), id) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/app/controllers/backoffice/roles_mock.go b/internal/app/controllers/backoffice/roles_mock.go deleted file mode 100644 index 19164c0..0000000 --- a/internal/app/controllers/backoffice/roles_mock.go +++ /dev/null @@ -1,101 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: internal/app/controllers/backoffice/roles.go -// -// Generated by this command: -// -// mockgen -source=internal/app/controllers/backoffice/roles.go -destination=internal/app/controllers/backoffice/roles_mock.go -package=backoffice -// - -// Package backoffice is a generated GoMock package. -package backoffice - -import ( - http "net/http" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockRolesController is a mock of RolesController interface. -type MockRolesController struct { - ctrl *gomock.Controller - recorder *MockRolesControllerMockRecorder - isgomock struct{} -} - -// MockRolesControllerMockRecorder is the mock recorder for MockRolesController. -type MockRolesControllerMockRecorder struct { - mock *MockRolesController -} - -// NewMockRolesController creates a new mock instance. -func NewMockBackofficeRolesController(ctrl *gomock.Controller) *MockRolesController { - mock := &MockRolesController{ctrl: ctrl} - mock.recorder = &MockRolesControllerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockRolesController) EXPECT() *MockRolesControllerMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *MockRolesController) Create(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Create", w, r) -} - -// Create indicates an expected call of Create. -func (mr *MockRolesControllerMockRecorder) Create(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockRolesController)(nil).Create), w, r) -} - -// Delete mocks base method. -func (m *MockRolesController) Delete(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", w, r) -} - -// Delete indicates an expected call of Delete. -func (mr *MockRolesControllerMockRecorder) Delete(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockRolesController)(nil).Delete), w, r) -} - -// Get mocks base method. -func (m *MockRolesController) Get(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Get", w, r) -} - -// Get indicates an expected call of Get. -func (mr *MockRolesControllerMockRecorder) Get(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRolesController)(nil).Get), w, r) -} - -// List mocks base method. -func (m *MockRolesController) List(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "List", w, r) -} - -// List indicates an expected call of List. -func (mr *MockRolesControllerMockRecorder) List(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockRolesController)(nil).List), w, r) -} - -// Update mocks base method. -func (m *MockRolesController) Update(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Update", w, r) -} - -// Update indicates an expected call of Update. -func (mr *MockRolesControllerMockRecorder) Update(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockRolesController)(nil).Update), w, r) -} diff --git a/internal/app/controllers/backoffice/roles_test.go b/internal/app/controllers/backoffice/roles_test.go deleted file mode 100644 index 620582d..0000000 --- a/internal/app/controllers/backoffice/roles_test.go +++ /dev/null @@ -1,571 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -func Test_Backoffice_Roles_List(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - rolesService := services.NewMockRoles(ctrl) - controller := NewRolesController(rolesService) - - type result struct { - response serializers.PaginationResponse[serializers.RoleSerializer] - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - rolesService.EXPECT().List(gomock.Any(), gomock.Any()).Return([]models.Role{ - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Admin role", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000002"), - Name: models.ManagerRoleType, - Description: "Manager role", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000003"), - Name: models.UserRoleType, - Description: "User role", - }, - }, uint64(3), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.RoleSerializer]{ - Data: []serializers.RoleSerializer{ - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Admin role", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000002"), - Name: models.ManagerRoleType, - Description: "Manager role", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000003"), - Name: models.UserRoleType, - Description: "User role", - }, - }, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: uint64(3), - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Empty", - before: func() { - rolesService.EXPECT().List(gomock.Any(), gomock.Any()).Return([]models.Role{}, uint64(0), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.RoleSerializer]{ - Data: []serializers.RoleSerializer{}, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: uint64(0), - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Error", - before: func() { - rolesService.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/roles", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/roles", controller.List) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PaginationResponse[serializers.RoleSerializer] - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Roles_Get(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - rolesService := services.NewMockRoles(ctrl) - controller := NewRolesController(rolesService) - - type result struct { - response serializers.RoleSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - rolesService.EXPECT().FindRoleDetailsById(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(&models.Role{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Admin role", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }, nil) - }, - expected: result{ - response: serializers.RoleSerializer{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Admin role", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Not found", - before: func() { - rolesService.EXPECT().FindRoleDetailsById(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(&models.Role{}, errors.ErrRoleNotFound) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: errors.ErrRoleNotFound.Error()}, - status: "404 Not Found", - code: http.StatusNotFound, - }, - error: true, - }, - { - name: "Error", - before: func() { - rolesService.EXPECT().FindRoleDetailsById(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(&models.Role{}, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/roles/10000000-1000-1000-1000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/roles/{id}", controller.Get) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.RoleSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Roles_Create(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - rolesService := services.NewMockRoles(ctrl) - controller := NewRolesController(rolesService) - - type result struct { - response serializers.RoleSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - rolesService.EXPECT().Create(gomock.Any(), &models.Role{ - Name: models.AdminRoleType, - Description: "Admin role", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }).Return(&models.Role{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Admin role", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }, nil) - }, - body: strings.NewReader(`{"name": "admin", "description": "Admin role", "permission_ids": ["10000000-1000-1000-3000-000000000001", "10000000-1000-1000-3000-000000000002"]}`), - expected: result{ - response: serializers.RoleSerializer{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Admin role", - }, - status: "201 Created", - code: http.StatusCreated, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - rolesService.EXPECT().Create(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"name": "admin"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty description"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - rolesService.EXPECT().Create(gomock.Any(), &models.Role{ - Name: models.AdminRoleType, - Description: "Admin role", - }).Return(&models.Role{}, assert.AnError) - }, - body: strings.NewReader(`{"name": "admin", "description": "Admin role"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPost, "/api/backoffice/roles", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Post("/api/backoffice/roles", controller.Create) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.RoleSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Roles_Update(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - rolesService := services.NewMockRoles(ctrl) - controller := NewRolesController(rolesService) - - type result struct { - response serializers.RoleSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - rolesService.EXPECT().Update(gomock.Any(), &models.Role{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Administrator role updated", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }).Return(&models.Role{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Administrator role updated", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }, nil) - }, - body: strings.NewReader(`{"name": "admin", "description": "Administrator role updated", "permission_ids": ["10000000-1000-1000-3000-000000000001", "10000000-1000-1000-3000-000000000002"]}`), - expected: result{ - response: serializers.RoleSerializer{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Administrator role updated", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - rolesService.EXPECT().Update(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"name": "admin"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty description"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - rolesService.EXPECT().Update(gomock.Any(), &models.Role{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - Name: models.AdminRoleType, - Description: "Administrator role updated", - PermissionIDs: []uuid.UUID{ - uuid.MustParse("10000000-1000-1000-3000-000000000001"), - uuid.MustParse("10000000-1000-1000-3000-000000000002"), - }, - }).Return(&models.Role{}, assert.AnError) - }, - body: strings.NewReader(`{"name": "admin", "description": "Administrator role updated", "permission_ids": ["10000000-1000-1000-3000-000000000001", "10000000-1000-1000-3000-000000000002"]}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPut, "/api/backoffice/roles/10000000-1000-1000-1000-000000000001", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Put("/api/backoffice/roles/{id}", controller.Update) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.RoleSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Roles_Delete(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - rolesService := services.NewMockRoles(ctrl) - controller := NewRolesController(rolesService) - - type result struct { - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - rolesService.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(true, nil) - }, - expected: result{ - status: "204 No Content", - code: http.StatusNoContent, - }, - error: false, - }, - { - name: "Error", - before: func() { - rolesService.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(false, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodDelete, "/api/backoffice/roles/10000000-1000-1000-1000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Delete("/api/backoffice/roles/{id}", controller.Delete) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} diff --git a/internal/app/controllers/backoffice/scopes.go b/internal/app/controllers/backoffice/scopes.go deleted file mode 100644 index c671181..0000000 --- a/internal/app/controllers/backoffice/scopes.go +++ /dev/null @@ -1,177 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/models/dto" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -type ScopesController interface { - List(w http.ResponseWriter, r *http.Request) - Get(w http.ResponseWriter, r *http.Request) - Create(w http.ResponseWriter, r *http.Request) - Update(w http.ResponseWriter, r *http.Request) - Delete(w http.ResponseWriter, r *http.Request) -} - -type scopesController struct { - scopes services.Scopes -} - -func NewScopesController(scopes services.Scopes) ScopesController { - return &scopesController{ - scopes: scopes, - } -} - -//nolint:dupl -func (c *scopesController) List(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - paginator := services.NewPagination(r) - rows, total, err := c.scopes.List(r.Context(), paginator) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - collection := make([]serializers.ScopeSerializer, 0, len(rows)) - - for _, row := range rows { - collection = append(collection, serializers.ScopeSerializer{ - ID: row.ID, - Name: row.Name, - Description: row.Description, - }) - } - - response := serializers.PaginationResponse[serializers.ScopeSerializer]{ - Data: collection, - Meta: serializers.PaginationMeta{ - Page: paginator.Page, - Per: paginator.PerPage, - Total: total, - }, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *scopesController) Get(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - record, err := c.scopes.FindById(r.Context(), id) - if err != nil { - if errors.Is(err, errors.ErrScopeNotFound) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.ScopeSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *scopesController) Create(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - var params dto.ScopeRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.scopes.Create(r.Context(), &models.Scope{ - Name: params.Name, - Description: params.Description, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.ScopeSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *scopesController) Update(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - var params dto.ScopeRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.scopes.Update(r.Context(), &models.Scope{ - ID: id, - Name: params.Name, - Description: params.Description, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.ScopeSerializer{ - ID: record.ID, - Name: record.Name, - Description: record.Description, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *scopesController) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - _, err := c.scopes.Delete(r.Context(), id) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/app/controllers/backoffice/scopes_mock.go b/internal/app/controllers/backoffice/scopes_mock.go deleted file mode 100644 index f969c95..0000000 --- a/internal/app/controllers/backoffice/scopes_mock.go +++ /dev/null @@ -1,101 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: internal/app/controllers/backoffice/scopes.go -// -// Generated by this command: -// -// mockgen -source=internal/app/controllers/backoffice/scopes.go -destination=internal/app/controllers/backoffice/scopes_mock.go -package=backoffice -// - -// Package backoffice is a generated GoMock package. -package backoffice - -import ( - http "net/http" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockScopesController is a mock of ScopesController interface. -type MockScopesController struct { - ctrl *gomock.Controller - recorder *MockScopesControllerMockRecorder - isgomock struct{} -} - -// MockScopesControllerMockRecorder is the mock recorder for MockScopesController. -type MockScopesControllerMockRecorder struct { - mock *MockScopesController -} - -// NewMockScopesController creates a new mock instance. -func NewMockBackofficeScopesController(ctrl *gomock.Controller) *MockScopesController { - mock := &MockScopesController{ctrl: ctrl} - mock.recorder = &MockScopesControllerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockScopesController) EXPECT() *MockScopesControllerMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *MockScopesController) Create(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Create", w, r) -} - -// Create indicates an expected call of Create. -func (mr *MockScopesControllerMockRecorder) Create(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockScopesController)(nil).Create), w, r) -} - -// Delete mocks base method. -func (m *MockScopesController) Delete(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", w, r) -} - -// Delete indicates an expected call of Delete. -func (mr *MockScopesControllerMockRecorder) Delete(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockScopesController)(nil).Delete), w, r) -} - -// Get mocks base method. -func (m *MockScopesController) Get(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Get", w, r) -} - -// Get indicates an expected call of Get. -func (mr *MockScopesControllerMockRecorder) Get(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockScopesController)(nil).Get), w, r) -} - -// List mocks base method. -func (m *MockScopesController) List(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "List", w, r) -} - -// List indicates an expected call of List. -func (mr *MockScopesControllerMockRecorder) List(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockScopesController)(nil).List), w, r) -} - -// Update mocks base method. -func (m *MockScopesController) Update(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Update", w, r) -} - -// Update indicates an expected call of Update. -func (mr *MockScopesControllerMockRecorder) Update(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockScopesController)(nil).Update), w, r) -} diff --git a/internal/app/controllers/backoffice/scopes_test.go b/internal/app/controllers/backoffice/scopes_test.go deleted file mode 100644 index 7632654..0000000 --- a/internal/app/controllers/backoffice/scopes_test.go +++ /dev/null @@ -1,532 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -func Test_Backoffice_Scopes_List(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - scopes := services.NewMockScopes(ctrl) - controller := NewScopesController(scopes) - - type result struct { - response serializers.PaginationResponse[serializers.ScopeSerializer] - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - scopes.EXPECT().List(gomock.Any(), gomock.Any()).Return([]models.Scope{ - { - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, - { - ID: uuid.MustParse("10000000-1000-1000-2000-000000000002"), - Name: "self-service", - Description: "Self-service scope", - }, - }, uint64(2), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.ScopeSerializer]{ - Data: []serializers.ScopeSerializer{ - { - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, - { - ID: uuid.MustParse("10000000-1000-1000-2000-000000000002"), - Name: "self-service", - Description: "Self-service scope", - }, - }, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: 2, - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Empty", - before: func() { - scopes.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.ScopeSerializer]{ - Data: []serializers.ScopeSerializer{}, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: 0, - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Error", - before: func() { - scopes.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/scopes", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/scopes", controller.List) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PaginationResponse[serializers.ScopeSerializer] - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Scopes_Get(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - scopes := services.NewMockScopes(ctrl) - controller := NewScopesController(scopes) - - type result struct { - response serializers.ScopeSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - scopes.EXPECT().FindById(gomock.Any(), uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(&models.Scope{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, nil) - }, - expected: result{ - response: serializers.ScopeSerializer{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Not found", - before: func() { - scopes.EXPECT().FindById(gomock.Any(), uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(&models.Scope{}, errors.ErrScopeNotFound) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: errors.ErrScopeNotFound.Error()}, - status: "404 Not Found", - code: http.StatusNotFound, - }, - }, - { - name: "Error", - before: func() { - scopes.EXPECT().FindById(gomock.Any(), uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(&models.Scope{}, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/scopes/10000000-1000-1000-2000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/scopes/{id}", controller.Get) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.ScopeSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Scopes_Create(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - scopes := services.NewMockScopes(ctrl) - controller := NewScopesController(scopes) - - type result struct { - response serializers.ScopeSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - scopes.EXPECT().Create(gomock.Any(), &models.Scope{ - Name: "sso-service", - Description: "SSO-service scope", - }).Return(&models.Scope{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, nil) - }, - body: strings.NewReader(`{"name": "sso-service", "description": "SSO-service scope"}`), - expected: result{ - response: serializers.ScopeSerializer{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, - status: "201 Created", - code: http.StatusCreated, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - scopes.EXPECT().Create(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"name": "sso-service"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty description"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - scopes.EXPECT().Create(gomock.Any(), &models.Scope{ - Name: "sso-service", - Description: "SSO-service scope", - }).Return(&models.Scope{}, assert.AnError) - }, - body: strings.NewReader(`{"name": "sso-service", "description": "SSO-service scope"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPost, "/api/backoffice/scopes", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Post("/api/backoffice/scopes", controller.Create) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.ScopeSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Scopes_Update(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - scopes := services.NewMockScopes(ctrl) - controller := NewScopesController(scopes) - - type result struct { - response serializers.ScopeSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - scopes.EXPECT().Update(gomock.Any(), &models.Scope{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }).Return(&models.Scope{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, nil) - }, - body: strings.NewReader(`{"name": "sso-service", "description": "SSO-service scope"}`), - expected: result{ - response: serializers.ScopeSerializer{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - scopes.EXPECT().Update(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"name": "sso-service"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty description"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - scopes.EXPECT().Update(gomock.Any(), &models.Scope{ - ID: uuid.MustParse("10000000-1000-1000-2000-000000000001"), - Name: "sso-service", - Description: "SSO-service scope", - }).Return(&models.Scope{}, assert.AnError) - }, - body: strings.NewReader(`{"name": "sso-service", "description": "SSO-service scope"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPut, "/api/backoffice/scopes/10000000-1000-1000-2000-000000000001", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Put("/api/backoffice/scopes/{id}", controller.Update) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.ScopeSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Scopes_Delete(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - scopes := services.NewMockScopes(ctrl) - controller := NewScopesController(scopes) - - type result struct { - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - scopes.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(true, nil) - }, - expected: result{ - status: "204 No Content", - code: http.StatusNoContent, - }, - error: false, - }, - { - name: "Error", - before: func() { - scopes.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-2000-000000000001")).Return(false, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodDelete, "/api/backoffice/scopes/10000000-1000-1000-2000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Delete("/api/backoffice/scopes/{id}", controller.Delete) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} diff --git a/internal/app/controllers/backoffice/tokens.go b/internal/app/controllers/backoffice/tokens.go deleted file mode 100644 index 8c16888..0000000 --- a/internal/app/controllers/backoffice/tokens.go +++ /dev/null @@ -1,80 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -type TokensController interface { - List(w http.ResponseWriter, r *http.Request) - Delete(w http.ResponseWriter, r *http.Request) -} - -type tokensController struct { - tokens services.Tokens -} - -func NewTokensController(tokens services.Tokens) TokensController { - return &tokensController{ - tokens: tokens, - } -} - -//nolint:dupl -func (c *tokensController) List(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - paginator := services.NewPagination(r) - rows, total, err := c.tokens.List(r.Context(), paginator) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - collection := make([]serializers.TokenSerializer, 0, len(rows)) - - for _, row := range rows { - collection = append(collection, serializers.TokenSerializer{ - ID: row.ID, - UserId: row.UserId, - Type: row.Type, - Value: row.Value, - ExpiresAt: row.ExpiresAt, - }) - } - - response := serializers.PaginationResponse[serializers.TokenSerializer]{ - Data: collection, - Meta: serializers.PaginationMeta{ - Page: paginator.Page, - Per: paginator.PerPage, - Total: total, - }, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *tokensController) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - _, err := c.tokens.Delete(r.Context(), id) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/app/controllers/backoffice/tokens_mock.go b/internal/app/controllers/backoffice/tokens_mock.go deleted file mode 100644 index 7e88046..0000000 --- a/internal/app/controllers/backoffice/tokens_mock.go +++ /dev/null @@ -1,65 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: internal/app/controllers/backoffice/tokens.go -// -// Generated by this command: -// -// mockgen -source=internal/app/controllers/backoffice/tokens.go -destination=internal/app/controllers/backoffice/tokens_mock.go -package=backoffice -// - -// Package backoffice is a generated GoMock package. -package backoffice - -import ( - http "net/http" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockTokensController is a mock of TokensController interface. -type MockTokensController struct { - ctrl *gomock.Controller - recorder *MockTokensControllerMockRecorder - isgomock struct{} -} - -// MockTokensControllerMockRecorder is the mock recorder for MockTokensController. -type MockTokensControllerMockRecorder struct { - mock *MockTokensController -} - -// NewMockTokensController creates a new mock instance. -func NewMockBackofficeTokensController(ctrl *gomock.Controller) *MockTokensController { - mock := &MockTokensController{ctrl: ctrl} - mock.recorder = &MockTokensControllerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTokensController) EXPECT() *MockTokensControllerMockRecorder { - return m.recorder -} - -// Delete mocks base method. -func (m *MockTokensController) Delete(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", w, r) -} - -// Delete indicates an expected call of Delete. -func (mr *MockTokensControllerMockRecorder) Delete(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockTokensController)(nil).Delete), w, r) -} - -// List mocks base method. -func (m *MockTokensController) List(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "List", w, r) -} - -// List indicates an expected call of List. -func (mr *MockTokensControllerMockRecorder) List(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockTokensController)(nil).List), w, r) -} diff --git a/internal/app/controllers/backoffice/tokens_test.go b/internal/app/controllers/backoffice/tokens_test.go deleted file mode 100644 index a10d94b..0000000 --- a/internal/app/controllers/backoffice/tokens_test.go +++ /dev/null @@ -1,218 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" - - "loki/internal/app/models" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -func Test_Backoffice_Tokens_List(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - tokens := services.NewMockTokens(ctrl) - controller := NewTokensController(tokens) - - type result struct { - response serializers.PaginationResponse[serializers.TokenSerializer] - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - tokens.EXPECT().List(gomock.Any(), gomock.Any()).Return([]models.Token{ - { - ID: uuid.MustParse("10000000-1000-1000-1111-000000000001"), - UserId: uuid.MustParse("10000000-1000-1000-2222-000000000001"), - Type: models.AccessTokenType, - Value: "access-token-value", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1111-000000000002"), - UserId: uuid.MustParse("10000000-1000-1000-2222-000000000002"), - Type: models.RefreshTokenType, - Value: "refresh-token-value", - }, - }, uint64(2), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.TokenSerializer]{ - Data: []serializers.TokenSerializer{ - { - ID: uuid.MustParse("10000000-1000-1000-1111-000000000001"), - UserId: uuid.MustParse("10000000-1000-1000-2222-000000000001"), - Type: models.AccessTokenType, - Value: "access-token-value", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1111-000000000002"), - UserId: uuid.MustParse("10000000-1000-1000-2222-000000000002"), - Type: models.RefreshTokenType, - Value: "refresh-token-value", - }, - }, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: 2, - }, - }, - status: "200 OK", - code: 200, - }, - error: false, - }, - { - name: "Empty", - before: func() { - tokens.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.TokenSerializer]{ - Data: []serializers.TokenSerializer{}, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: 0, - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Error", - before: func() { - tokens.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: 422, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/tokens", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/tokens", controller.List) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PaginationResponse[serializers.TokenSerializer] - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Tokens_Delete(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - tokens := services.NewMockTokens(ctrl) - controller := NewTokensController(tokens) - - type result struct { - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - tokens.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-1111-000000000001")).Return(true, nil) - }, - expected: result{ - status: "204 No Content", - code: http.StatusNoContent, - }, - error: false, - }, - { - name: "Error", - before: func() { - tokens.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-1111-000000000001")).Return(false, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodDelete, "/api/backoffice/tokens/10000000-1000-1000-1111-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Delete("/api/backoffice/tokens/{id}", controller.Delete) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} diff --git a/internal/app/controllers/backoffice/users.go b/internal/app/controllers/backoffice/users.go deleted file mode 100644 index 7f0212a..0000000 --- a/internal/app/controllers/backoffice/users.go +++ /dev/null @@ -1,193 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/models/dto" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -type UsersController interface { - List(w http.ResponseWriter, r *http.Request) - Get(w http.ResponseWriter, r *http.Request) - Create(w http.ResponseWriter, r *http.Request) - Update(w http.ResponseWriter, r *http.Request) - Delete(w http.ResponseWriter, r *http.Request) -} - -type usersController struct { - users services.Users -} - -func NewUsersController(users services.Users) UsersController { - return &usersController{ - users: users, - } -} - -//nolint:dupl -func (c *usersController) List(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - paginator := services.NewPagination(r) - rows, total, err := c.users.List(r.Context(), paginator) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - collection := make([]serializers.UserSerializer, 0, len(rows)) - - for _, row := range rows { - collection = append(collection, serializers.UserSerializer{ - ID: row.ID, - IdentityNumber: row.IdentityNumber, - PersonalCode: row.PersonalCode, - FirstName: row.FirstName, - LastName: row.LastName, - }) - } - - response := serializers.PaginationResponse[serializers.UserSerializer]{ - Data: collection, - Meta: serializers.PaginationMeta{ - Page: paginator.Page, - Per: paginator.PerPage, - Total: total, - }, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *usersController) Get(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - record, err := c.users.FindUserDetailsById(r.Context(), id) - if err != nil { - if errors.Is(err, errors.ErrUserNotFound) { - w.WriteHeader(http.StatusNotFound) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.UserSerializer{ - ID: record.ID, - IdentityNumber: record.IdentityNumber, - PersonalCode: record.PersonalCode, - FirstName: record.FirstName, - LastName: record.LastName, - RoleIDs: record.RoleIDs, - ScopeIDs: record.ScopeIDs, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *usersController) Create(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - var params dto.UserRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.users.Create(r.Context(), &models.User{ - IdentityNumber: params.IdentityNumber, - PersonalCode: params.PersonalCode, - FirstName: params.FirstName, - LastName: params.LastName, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.UserSerializer{ - ID: record.ID, - IdentityNumber: record.IdentityNumber, - PersonalCode: record.PersonalCode, - FirstName: record.FirstName, - LastName: record.LastName, - } - - w.WriteHeader(http.StatusCreated) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *usersController) Update(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - var params dto.UserRequest - if err := params.Validate(r.Body); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - record, err := c.users.Update(r.Context(), &models.User{ - ID: id, - IdentityNumber: params.IdentityNumber, - PersonalCode: params.PersonalCode, - FirstName: params.FirstName, - LastName: params.LastName, - RoleIDs: params.RoleIDs, - ScopeIDs: params.ScopeIDs, - }) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - response := serializers.UserSerializer{ - ID: record.ID, - IdentityNumber: record.IdentityNumber, - PersonalCode: record.PersonalCode, - FirstName: record.FirstName, - LastName: record.LastName, - } - - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(response) -} - -//nolint:dupl -func (c *usersController) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - id := uuid.MustParse(chi.URLParam(r, "id")) - - _, err := c.users.Delete(r.Context(), id) - if err != nil { - w.WriteHeader(http.StatusUnprocessableEntity) - _ = json.NewEncoder(w).Encode(serializers.ErrorSerializer{Error: err.Error()}) - return - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/app/controllers/backoffice/users_mock.go b/internal/app/controllers/backoffice/users_mock.go deleted file mode 100644 index 335f093..0000000 --- a/internal/app/controllers/backoffice/users_mock.go +++ /dev/null @@ -1,101 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: internal/app/controllers/backoffice/users.go -// -// Generated by this command: -// -// mockgen -source=internal/app/controllers/backoffice/users.go -destination=internal/app/controllers/backoffice/users_mock.go -package=backoffice -// - -// Package backoffice is a generated GoMock package. -package backoffice - -import ( - http "net/http" - reflect "reflect" - - gomock "go.uber.org/mock/gomock" -) - -// MockUsersController is a mock of UsersController interface. -type MockUsersController struct { - ctrl *gomock.Controller - recorder *MockUsersControllerMockRecorder - isgomock struct{} -} - -// MockUsersControllerMockRecorder is the mock recorder for MockUsersController. -type MockUsersControllerMockRecorder struct { - mock *MockUsersController -} - -// NewMockUsersController creates a new mock instance. -func NewMockBackofficeUsersController(ctrl *gomock.Controller) *MockUsersController { - mock := &MockUsersController{ctrl: ctrl} - mock.recorder = &MockUsersControllerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockUsersController) EXPECT() *MockUsersControllerMockRecorder { - return m.recorder -} - -// Create mocks base method. -func (m *MockUsersController) Create(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Create", w, r) -} - -// Create indicates an expected call of Create. -func (mr *MockUsersControllerMockRecorder) Create(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockUsersController)(nil).Create), w, r) -} - -// Delete mocks base method. -func (m *MockUsersController) Delete(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Delete", w, r) -} - -// Delete indicates an expected call of Delete. -func (mr *MockUsersControllerMockRecorder) Delete(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockUsersController)(nil).Delete), w, r) -} - -// Get mocks base method. -func (m *MockUsersController) Get(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Get", w, r) -} - -// Get indicates an expected call of Get. -func (mr *MockUsersControllerMockRecorder) Get(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockUsersController)(nil).Get), w, r) -} - -// List mocks base method. -func (m *MockUsersController) List(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "List", w, r) -} - -// List indicates an expected call of List. -func (mr *MockUsersControllerMockRecorder) List(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockUsersController)(nil).List), w, r) -} - -// Update mocks base method. -func (m *MockUsersController) Update(w http.ResponseWriter, r *http.Request) { - m.ctrl.T.Helper() - m.ctrl.Call(m, "Update", w, r) -} - -// Update indicates an expected call of Update. -func (mr *MockUsersControllerMockRecorder) Update(w, r any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUsersController)(nil).Update), w, r) -} diff --git a/internal/app/controllers/backoffice/users_test.go b/internal/app/controllers/backoffice/users_test.go deleted file mode 100644 index 4379d65..0000000 --- a/internal/app/controllers/backoffice/users_test.go +++ /dev/null @@ -1,560 +0,0 @@ -package backoffice - -import ( - "encoding/json" - "io" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "go.uber.org/mock/gomock" - - "loki/internal/app/errors" - "loki/internal/app/models" - "loki/internal/app/serializers" - "loki/internal/app/services" -) - -func Test_Backoffice_Users_List(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - users := services.NewMockUsers(ctrl) - controller := NewUsersController(users) - - type result struct { - response serializers.PaginationResponse[serializers.UserSerializer] - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - users.EXPECT().List(gomock.Any(), gomock.Any()).Return([]models.User{ - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000002"), - IdentityNumber: "PNOEE-987654321", - PersonalCode: "987654321", - FirstName: "Jane", - LastName: "Doe", - }, - }, uint64(2), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.UserSerializer]{ - Data: []serializers.UserSerializer{ - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }, - { - ID: uuid.MustParse("10000000-1000-1000-1000-000000000002"), - IdentityNumber: "PNOEE-987654321", - PersonalCode: "987654321", - FirstName: "Jane", - LastName: "Doe", - }, - }, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: 2, - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Empty", - before: func() { - users.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), nil) - }, - expected: result{ - response: serializers.PaginationResponse[serializers.UserSerializer]{ - Data: []serializers.UserSerializer{}, - Meta: serializers.PaginationMeta{ - Page: 1, - Per: 25, - Total: 0, - }, - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Error", - before: func() { - users.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, uint64(0), assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/users", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/users", controller.List) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.PaginationResponse[serializers.UserSerializer] - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Users_Get(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - users := services.NewMockUsers(ctrl) - controller := NewUsersController(users) - - type result struct { - response serializers.UserSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - users.EXPECT().FindUserDetailsById(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(&models.User{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }, nil) - }, - expected: result{ - response: serializers.UserSerializer{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Not found", - before: func() { - users.EXPECT().FindUserDetailsById(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(&models.User{}, errors.ErrUserNotFound) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: errors.ErrUserNotFound.Error()}, - status: "404 Not Found", - code: http.StatusNotFound, - }, - }, - { - name: "Error", - before: func() { - users.EXPECT().FindUserDetailsById(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(&models.User{}, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodGet, "/api/backoffice/users/10000000-1000-1000-1000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Get("/api/backoffice/users/{id}", controller.Get) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.UserSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Users_Create(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - users := services.NewMockUsers(ctrl) - controller := NewUsersController(users) - - type result struct { - response serializers.UserSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - users.EXPECT().Create(gomock.Any(), &models.User{ - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }).Return(&models.User{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }, nil) - }, - body: strings.NewReader(`{"identity_number": "PNOEE-123456789", "personal_code": "123456789", "first_name": "John", "last_name": "Doe"}`), - expected: result{ - response: serializers.UserSerializer{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }, - status: "201 Created", - code: http.StatusCreated, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - users.EXPECT().Create(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"identity_number": "PNOEE-123456789"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty personal code"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - users.EXPECT().Create(gomock.Any(), &models.User{ - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }).Return(&models.User{}, assert.AnError) - }, - body: strings.NewReader(`{"identity_number": "PNOEE-123456789", "personal_code": "123456789", "first_name": "John", "last_name": "Doe"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPost, "/api/backoffice/users", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Post("/api/backoffice/users", controller.Create) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.UserSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Users_Update(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - users := services.NewMockUsers(ctrl) - controller := NewUsersController(users) - - type result struct { - response serializers.UserSerializer - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - body io.Reader - expected result - error bool - }{ - { - name: "Success", - before: func() { - users.EXPECT().Update(gomock.Any(), &models.User{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "JOHN", - LastName: "DOE", - }).Return(&models.User{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "JOHN", - LastName: "DOE", - }, nil) - }, - body: strings.NewReader(`{"identity_number": "PNOEE-123456789", "personal_code": "123456789", "first_name": "JOHN", "last_name":"DOE"}`), - expected: result{ - response: serializers.UserSerializer{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "JOHN", - LastName: "DOE", - }, - status: "200 OK", - code: http.StatusOK, - }, - error: false, - }, - { - name: "Invalid params", - before: func() { - users.EXPECT().Update(gomock.Any(), gomock.Any()).Times(0) - }, - body: strings.NewReader(`{"identity_number": "PNOEE-123456789"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: "empty personal code"}, - status: "400 Bad Request", - code: http.StatusBadRequest, - }, - error: true, - }, - { - name: "Error", - before: func() { - users.EXPECT().Update(gomock.Any(), &models.User{ - ID: uuid.MustParse("10000000-1000-1000-1000-000000000001"), - IdentityNumber: "PNOEE-123456789", - PersonalCode: "123456789", - FirstName: "John", - LastName: "Doe", - }).Return(&models.User{}, assert.AnError) - }, - body: strings.NewReader(`{"identity_number": "PNOEE-123456789", "personal_code": "123456789", "first_name": "John", "last_name": "Doe"}`), - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodPut, "/api/backoffice/users/10000000-1000-1000-1000-000000000001", tt.body) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Put("/api/backoffice/users/{id}", controller.Update) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } else { - var response serializers.UserSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.response, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} - -func Test_Backoffice_Users_Delete(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - users := services.NewMockUsers(ctrl) - controller := NewUsersController(users) - - type result struct { - error serializers.ErrorSerializer - status string - code int - } - - tests := []struct { - name string - before func() - expected result - error bool - }{ - { - name: "Success", - before: func() { - users.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(true, nil) - }, - expected: result{ - status: "204 No Content", - code: http.StatusNoContent, - }, - error: false, - }, - { - name: "Error", - before: func() { - users.EXPECT().Delete(gomock.Any(), uuid.MustParse("10000000-1000-1000-1000-000000000001")).Return(false, assert.AnError) - }, - expected: result{ - error: serializers.ErrorSerializer{Error: assert.AnError.Error()}, - status: "422 Unprocessable Entity", - code: http.StatusUnprocessableEntity, - }, - error: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tt.before() - - req := httptest.NewRequest(http.MethodDelete, "/api/backoffice/users/10000000-1000-1000-1000-000000000001", nil) - w := httptest.NewRecorder() - - r := chi.NewRouter() - r.Delete("/api/backoffice/users/{id}", controller.Delete) - r.ServeHTTP(w, req) - - resp := w.Result() - defer resp.Body.Close() - - if tt.error { - var response serializers.ErrorSerializer - err := json.NewDecoder(resp.Body).Decode(&response) - assert.NoError(t, err) - assert.Equal(t, tt.expected.error, response) - } - - assert.Equal(t, tt.expected.code, resp.StatusCode) - assert.Equal(t, tt.expected.status, resp.Status) - }) - } -} diff --git a/internal/config/router/router.go b/internal/config/router/router.go index ffdff36..6d88291 100644 --- a/internal/config/router/router.go +++ b/internal/config/router/router.go @@ -8,17 +8,14 @@ import ( "github.com/go-chi/cors" "loki/internal/app/controllers" - "loki/internal/app/controllers/backoffice" "loki/internal/config" "loki/internal/config/middlewares" - "loki/pkg/rbac" ) func NewRouter( cfg *config.Config, authentication middlewares.AuthenticationMiddleware, - authorization middlewares.AuthorizationMiddleware, telemetry middlewares.TelemetryMiddleware, logger middlewares.LoggerMiddleware, @@ -28,12 +25,6 @@ func NewRouter( sessions controllers.SessionsController, tokens controllers.TokensController, users controllers.UsersController, - - backofficePermissions backoffice.PermissionsController, - backofficeRoles backoffice.RolesController, - backofficeScopes backoffice.ScopesController, - backofficeTokens backoffice.TokensController, - backofficeUsers backoffice.UsersController, ) http.Handler { r := chi.NewRouter() @@ -69,38 +60,5 @@ func NewRouter( r.Get("/api/me", users.Me) }) - r.Group(func(r chi.Router) { - r.Use(authorization.Authorize) - - r.Route("/api/backoffice", func(r chi.Router) { - r.With(authorization.Check(rbac.ReadPermissions)).Get("/permissions", backofficePermissions.List) - r.With(authorization.Check(rbac.ReadPermissions)).Get("/permissions/{id}", backofficePermissions.Get) - r.With(authorization.Check(rbac.WritePermissions)).Post("/permissions", backofficePermissions.Create) - r.With(authorization.Check(rbac.WritePermissions)).Put("/permissions/{id}", backofficePermissions.Update) - r.With(authorization.Check(rbac.WritePermissions)).Delete("/permissions/{id}", backofficePermissions.Delete) - - r.With(authorization.Check(rbac.ReadRoles)).Get("/roles", backofficeRoles.List) - r.With(authorization.Check(rbac.ReadRoles)).Get("/roles/{id}", backofficeRoles.Get) - r.With(authorization.Check(rbac.WriteRoles)).Post("/roles", backofficeRoles.Create) - r.With(authorization.Check(rbac.WriteRoles)).Put("/roles/{id}", backofficeRoles.Update) - r.With(authorization.Check(rbac.WriteRoles)).Delete("/roles/{id}", backofficeRoles.Delete) - - r.With(authorization.Check(rbac.ReadScopes)).Get("/scopes", backofficeScopes.List) - r.With(authorization.Check(rbac.ReadScopes)).Get("/scopes/{id}", backofficeScopes.Get) - r.With(authorization.Check(rbac.WriteScopes)).Post("/scopes", backofficeScopes.Create) - r.With(authorization.Check(rbac.WriteScopes)).Put("/scopes/{id}", backofficeScopes.Update) - r.With(authorization.Check(rbac.WriteScopes)).Delete("/scopes/{id}", backofficeScopes.Delete) - - r.With(authorization.Check(rbac.ReadTokens)).Get("/tokens", backofficeTokens.List) - r.With(authorization.Check(rbac.WriteTokens)).Delete("/tokens/{id}", backofficeTokens.Delete) - - r.With(authorization.Check(rbac.ReadUsers)).Get("/users", backofficeUsers.List) - r.With(authorization.Check(rbac.ReadUsers)).Get("/users/{id}", backofficeUsers.Get) - r.With(authorization.Check(rbac.WriteUsers)).Post("/users", backofficeUsers.Create) - r.With(authorization.Check(rbac.WriteUsers)).Put("/users/{id}", backofficeUsers.Update) - r.With(authorization.Check(rbac.WriteUsers)).Delete("/users/{id}", backofficeUsers.Delete) - }) - }) - return r } diff --git a/internal/config/router/router_test.go b/internal/config/router/router_test.go index 3d91eb2..182cc28 100644 --- a/internal/config/router/router_test.go +++ b/internal/config/router/router_test.go @@ -9,7 +9,6 @@ import ( "go.uber.org/mock/gomock" "loki/internal/app/controllers" - "loki/internal/app/controllers/backoffice" "loki/internal/config" "loki/internal/config/middlewares" ) @@ -24,7 +23,6 @@ func Test_HealthCheck(t *testing.T) { } mockAuthenticationMiddleware := middlewares.NewMockAuthenticationMiddleware(ctrl) - mockAuthorizationMiddleware := middlewares.NewMockAuthorizationMiddleware(ctrl) mockTelemetryMiddleware := middlewares.NewMockTelemetryMiddleware(ctrl) mockLoggerMiddleware := middlewares.NewMockLoggerMiddleware(ctrl) @@ -35,32 +33,12 @@ func Test_HealthCheck(t *testing.T) { mockTokensController := controllers.NewMockTokensController(ctrl) mockUsersController := controllers.NewMockUsersController(ctrl) - mockBackofficePermissionsController := backoffice.NewMockBackofficePermissionsController(ctrl) - mockBackofficeRolesController := backoffice.NewMockBackofficeRolesController(ctrl) - mockBackofficeScopesController := backoffice.NewMockBackofficeScopesController(ctrl) - mockBackofficeTokensController := backoffice.NewMockBackofficeTokensController(ctrl) - mockBackofficeUsersController := backoffice.NewMockBackofficeUsersController(ctrl) - mockAuthenticationMiddleware.EXPECT(). Authenticate(gomock.Any()). AnyTimes(). DoAndReturn(func(next http.Handler) http.Handler { return next }) - mockAuthorizationMiddleware.EXPECT(). - Authorize(gomock.Any()). - AnyTimes(). - DoAndReturn(func(next http.Handler) http.Handler { - return next - }) - mockAuthorizationMiddleware.EXPECT(). - Check(gomock.Any()). - AnyTimes(). - DoAndReturn(func(permission string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return next - } - }) mockTelemetryMiddleware.EXPECT(). Trace(gomock.Any()). AnyTimes(). @@ -77,7 +55,6 @@ func Test_HealthCheck(t *testing.T) { router := NewRouter( cfg, mockAuthenticationMiddleware, - mockAuthorizationMiddleware, mockTelemetryMiddleware, mockLoggerMiddleware, mockHealthController, @@ -86,11 +63,6 @@ func Test_HealthCheck(t *testing.T) { mockSessionsController, mockTokensController, mockUsersController, - mockBackofficePermissionsController, - mockBackofficeRolesController, - mockBackofficeScopesController, - mockBackofficeTokensController, - mockBackofficeUsersController, ) req := httptest.NewRequest(http.MethodHead, "/health", nil) diff --git a/internal/config/server/server_test.go b/internal/config/server/server_test.go index 8bfb6f1..45abf8c 100644 --- a/internal/config/server/server_test.go +++ b/internal/config/server/server_test.go @@ -10,7 +10,6 @@ import ( "go.uber.org/mock/gomock" "loki/internal/app/controllers" - "loki/internal/app/controllers/backoffice" "loki/internal/config" "loki/internal/config/middlewares" "loki/internal/config/router" @@ -26,7 +25,6 @@ func Test_NewWebServer(t *testing.T) { } mockAuthenticationMiddleware := middlewares.NewMockAuthenticationMiddleware(ctrl) - mockAuthorizationMiddleware := middlewares.NewMockAuthorizationMiddleware(ctrl) mockTelemetryMiddleware := middlewares.NewMockTelemetryMiddleware(ctrl) mockLoggerMiddleware := middlewares.NewMockLoggerMiddleware(ctrl) @@ -37,32 +35,12 @@ func Test_NewWebServer(t *testing.T) { mockTokensController := controllers.NewMockTokensController(ctrl) mockUsersController := controllers.NewMockUsersController(ctrl) - mockBackofficePermissionsController := backoffice.NewMockBackofficePermissionsController(ctrl) - mockBackofficeRolesController := backoffice.NewMockBackofficeRolesController(ctrl) - mockBackofficeScopesController := backoffice.NewMockBackofficeScopesController(ctrl) - mockBackofficeTokensController := backoffice.NewMockBackofficeTokensController(ctrl) - mockBackofficeUsersController := backoffice.NewMockBackofficeUsersController(ctrl) - mockAuthenticationMiddleware.EXPECT(). Authenticate(gomock.Any()). AnyTimes(). DoAndReturn(func(next http.Handler) http.Handler { return next }) - mockAuthorizationMiddleware.EXPECT(). - Authorize(gomock.Any()). - AnyTimes(). - DoAndReturn(func(next http.Handler) http.Handler { - return next - }) - mockAuthorizationMiddleware.EXPECT(). - Check(gomock.Any()). - AnyTimes(). - DoAndReturn(func(permission string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return next - } - }) mockTelemetryMiddleware.EXPECT(). Trace(gomock.Any()). AnyTimes(). @@ -79,7 +57,6 @@ func Test_NewWebServer(t *testing.T) { appRouter := router.NewRouter( cfg, mockAuthenticationMiddleware, - mockAuthorizationMiddleware, mockTelemetryMiddleware, mockLoggerMiddleware, mockHealthController, @@ -88,11 +65,6 @@ func Test_NewWebServer(t *testing.T) { mockSessionsController, mockTokensController, mockUsersController, - mockBackofficePermissionsController, - mockBackofficeRolesController, - mockBackofficeScopesController, - mockBackofficeTokensController, - mockBackofficeUsersController, ) srv := NewWebServer(cfg, appRouter) From bcf853807b34ba20b657394fee8129200dfee81a Mon Sep 17 00:00:00 2001 From: tab Date: Fri, 11 Apr 2025 11:26:37 +0300 Subject: [PATCH 16/20] feat(api): Remove backoffice API endpoints from OpenAPI specification --- api/swagger.yaml | 1184 ++-------------------------------------------- 1 file changed, 35 insertions(+), 1149 deletions(-) diff --git a/api/swagger.yaml b/api/swagger.yaml index 07e659f..3ffa628 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -13,6 +13,10 @@ paths: tags: - smart_id parameters: + - name: X-Request-ID + in: header + schema: + $ref: "#/components/schemas/RequestId" - name: X-Trace-ID in: header schema: @@ -105,6 +109,10 @@ paths: tags: - mobile_id parameters: + - name: X-Request-ID + in: header + schema: + $ref: "#/components/schemas/RequestId" - name: X-Trace-ID in: header schema: @@ -197,6 +205,10 @@ paths: schema: type: string description: "Session ID" + - name: X-Request-ID + in: header + schema: + $ref: "#/components/schemas/RequestId" - name: X-Trace-ID in: header schema: @@ -232,6 +244,10 @@ paths: schema: type: string description: "Session ID" + - name: X-Request-ID + in: header + schema: + $ref: "#/components/schemas/RequestId" - name: X-Trace-ID in: header schema: @@ -257,6 +273,10 @@ paths: tags: - tokens parameters: + - name: X-Request-ID + in: header + schema: + $ref: "#/components/schemas/RequestId" - name: X-Trace-ID in: header schema: @@ -296,47 +316,14 @@ paths: tags: - user parameters: - - name: X-Trace-ID + - name: X-Request-ID in: header schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/UserSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/permissions: - get: - summary: "List permissions" - description: "Retrieves a paginated list of permissions" - tags: - - backoffice - parameters: + $ref: "#/components/schemas/RequestId" - name: X-Trace-ID in: header schema: $ref: "#/components/schemas/TraceId" - - name: page - in: query - schema: - type: integer - description: "Page number for pagination" - - name: per - in: query - schema: - type: integer - description: "Number of items per page" security: - Authentication: [] responses: @@ -345,928 +332,30 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/PermissionsListResponse" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - post: - summary: "Create a permission" - description: "Creates a new permission" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/PermissionRequest" - responses: - "201": - description: "Created" - content: - application/json: - schema: - $ref: "#/components/schemas/PermissionSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" + $ref: "#/components/schemas/UserSerializer" "401": description: "Unauthorized" content: application/json: schema: $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - /api/backoffice/permissions/{id}: - get: - summary: "Get a permission" - description: "Retrieves a permission by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Permission ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/PermissionSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "404": - description: "Not Found" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - put: - summary: "Update a permission" - description: "Updates a permission by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Permission ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/PermissionRequest" - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/PermissionSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - delete: - summary: "Delete a permission" - description: "Deletes a permission by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Permission ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "204": - description: "No Content" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" +components: + securitySchemes: + Authentication: + type: http + scheme: Bearer + schemas: + RequestId: + type: string + format: uuid + example: "123e4567-e89b-12d3-a456-426614174001" + description: "Unique request identifier" - /api/backoffice/roles: - get: - summary: "List roles" - description: "Retrieves a paginated list of roles" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - - name: page - in: query - schema: - type: integer - description: "Page number for pagination" - - name: per - in: query - schema: - type: integer - description: "Number of items per page" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/RolesListResponse" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - post: - summary: "Create a role" - description: "Creates a new role" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/RoleRequest" - responses: - "201": - description: "Created" - content: - application/json: - schema: - $ref: "#/components/schemas/RoleSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/roles/{id}: - get: - summary: "Get a role" - description: "Retrieves a role by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Role ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/RoleSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "404": - description: "Not Found" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - put: - summary: "Update a role" - description: "Updates a role by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Role ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/RoleRequest" - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/RoleSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - delete: - summary: "Delete a role" - description: "Deletes a role by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Role ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "204": - description: "No Content" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/scopes: - get: - summary: "List scopes" - description: "Retrieves a paginated list of scopes" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - - name: page - in: query - schema: - type: integer - description: "Page number for pagination" - - name: per - in: query - schema: - type: integer - description: "Number of items per page" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScopesListResponse" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - post: - summary: "Create a scope" - description: "Creates a new scope" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/ScopeRequest" - responses: - "201": - description: "Created" - content: - application/json: - schema: - $ref: "#/components/schemas/ScopeSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/scopes/{id}: - get: - summary: "Get a scope" - description: "Retrieves a scope by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Scope ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScopeSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "404": - description: "Not Found" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - put: - summary: "Update a scope" - description: "Updates a scope by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Scope ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/ScopeRequest" - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/ScopeSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - delete: - summary: "Delete a scope" - description: "Deletes a scope by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Scope ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "204": - description: "No Content" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/tokens: - get: - summary: "List tokens" - description: "Retrieves a paginated list of tokens" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - - name: page - in: query - schema: - type: integer - description: "Page number for pagination" - - name: per - in: query - schema: - type: integer - description: "Number of items per page" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/TokensListResponse" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - delete: - summary: "Delete a token" - description: "Deletes a token by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "Token ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "204": - description: "No Content" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/users: - get: - summary: "List users" - description: "Retrieves a paginated list of users" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - - name: page - in: query - schema: - type: integer - description: "Page number for pagination" - - name: per - in: query - schema: - type: integer - description: "Number of items per page" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/UsersListResponse" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - post: - summary: "Create a user" - description: "Creates a new user with assigned role" - tags: - - backoffice - parameters: - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/UserRequest" - responses: - "201": - description: "Created" - content: - application/json: - schema: - $ref: "#/components/schemas/UserSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - - /api/backoffice/users/{id}: - get: - summary: "Get a user" - description: "Retrieves a user by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "User ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/UserSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "404": - description: "Not Found" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - put: - summary: "Update a user" - description: "Updates a user by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "User ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: "#/components/schemas/UserRequest" - responses: - "200": - description: "OK" - content: - application/json: - schema: - $ref: "#/components/schemas/UserSerializer" - "400": - description: "Bad Request" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - delete: - summary: "Delete a user" - description: "Deletes a user by its ID" - tags: - - backoffice - parameters: - - name: id - in: path - required: true - schema: - type: string - format: uuid - description: "User ID" - - name: X-Trace-ID - in: header - schema: - $ref: "#/components/schemas/TraceId" - security: - - Authentication: [] - responses: - "204": - description: "No Content" - "401": - description: "Unauthorized" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - "422": - description: "Unprocessable Entity" - content: - application/json: - schema: - $ref: "#/components/schemas/ErrorSerializer" - -components: - securitySchemes: - Authentication: - type: http - scheme: Bearer - schemas: TraceId: type: string format: uuid - example: "123e4567-e89b-12d3-a456-426614174000" + example: "123e4567-e89b-12d3-a456-426614174002" description: "Unique trace identifier" CreateSmartIdSessionRequest: @@ -1305,73 +394,6 @@ components: required: - refresh_token - PermissionRequest: - type: object - properties: - name: - type: string - description: "Updated name of the permission" - description: - type: string - description: "Updated description of the permission" - required: - - name - - description - - RoleRequest: - type: object - properties: - name: - type: string - description: "Name of the role" - description: - type: string - description: "Description of the role" - required: - - name - - description - - ScopeRequest: - type: object - properties: - name: - type: string - description: "Name of the scope" - description: - type: string - description: "Description of the scope" - required: - - name - - description - - UserRequest: - type: object - properties: - first_name: - type: string - description: "User's first name" - last_name: - type: string - description: "User's last name" - email: - type: string - format: email - description: "User's email address" - password: - type: string - format: password - description: "User's password" - role_id: - type: string - format: uuid - description: "Role ID to assign to the user" - required: - - first_name - - last_name - - email - - password - - role_id - SessionSerializer: type: object properties: @@ -1438,142 +460,6 @@ components: - access_token - refresh_token - PermissionSerializer: - type: object - properties: - id: - type: string - format: uuid - description: "Permission's unique ID" - name: - type: string - description: "Permission name" - description: - type: string - description: "Permission description" - required: - - id - - name - - description - - RoleSerializer: - type: object - properties: - id: - type: string - format: uuid - description: "Role's unique ID" - name: - type: string - description: "Role name" - description: - type: string - description: "Role description" - required: - - id - - name - - description - - ScopeSerializer: - type: object - properties: - id: - type: string - format: uuid - description: "Scope's unique ID" - name: - type: string - description: "Scope name" - description: - type: string - description: "Scope description" - required: - - id - - name - - description - - PaginationMeta: - type: object - properties: - page: - type: integer - description: "Current page number" - per: - type: integer - description: "Number of items per page" - total: - type: integer - description: "Total number of items" - required: - - page - - per - - total - - PermissionsListResponse: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/PermissionSerializer" - meta: - $ref: "#/components/schemas/PaginationMeta" - required: - - data - - meta - - RolesListResponse: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/RoleSerializer" - meta: - $ref: "#/components/schemas/PaginationMeta" - required: - - data - - meta - - ScopesListResponse: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/ScopeSerializer" - meta: - $ref: "#/components/schemas/PaginationMeta" - required: - - data - - meta - - TokensListResponse: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/TokensSerializer" - meta: - $ref: "#/components/schemas/PaginationMeta" - required: - - data - - meta - - UsersListResponse: - type: object - properties: - data: - type: array - items: - $ref: "#/components/schemas/UserSerializer" - meta: - $ref: "#/components/schemas/PaginationMeta" - required: - - data - - meta - ErrorSerializer: type: object properties: From fe4b258f7a3dc89b5c2ebb347990c7da90e08048 Mon Sep 17 00:00:00 2001 From: tab Date: Sat, 12 Apr 2025 22:24:00 +0300 Subject: [PATCH 17/20] docs(README): Update README Added sections for generating JWT signing keys and mTLS certificates Added related repositories section --- README.md | 102 ++++++++++++++++++++++++++++++++++++++++--- docs/index.md | 1 + docs/installation.md | 53 +++++++++++++++++++--- docs/repositories.md | 10 +++++ docs/usage.md | 5 +++ 5 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 docs/repositories.md diff --git a/README.md b/README.md index 40a546e..e68b483 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,13 @@ Before starting this application, you must have the loki-infrastructure running: ```sh git clone git@github.com/tab/loki-infrastructure.git cd loki-infrastructure -``` -```sh docker-compose up ``` ## Setup and Configuration -**Environment Variables**: +### Environment Variables Use `.env` files (e.g., `.env.development`) or provide environment variables for: @@ -37,7 +35,84 @@ Use `.env` files (e.g., `.env.development`) or provide environment variables for - `SMART_ID_API_URL`, `MOBILE_ID_API_URL` and corresponding relying on party credentials - `TELEMETRY_URI` for OpenTelemetry -**Database Migrations**: +### Generate Certificates and Keys + +Before running the services, you need to generate certificates for mTLS and keys for JWT signing: + +#### JWT Signing Keys + +```sh +mkdir -p certs/jwt + +openssl genrsa -out certs/jwt/private.key 4096 +openssl rsa -in certs/jwt/private.key -pubout -out certs/jwt/public.key +``` + +#### mTLS Certificates + +```sh +# Generate CA +openssl genrsa -out certs/ca.key 4096 +openssl req -new -x509 -key certs/ca.key -sha256 -subj "/CN=Loki CA" -out certs/ca.pem -days 3650 + +# Generate Server Certificate +openssl genrsa -out certs/server.key 4096 +openssl req -new -key certs/server.key -out certs/server.csr -config <( +cat <<-EOF +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +req_extensions = req_ext +distinguished_name = dn + +[dn] +CN = loki-backend + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = backend +IP.1 = 127.0.0.1 +IP.2 = 0.0.0.0 +EOF +) + +openssl x509 -req -in certs/server.csr -CA certs/ca.pem -CAkey certs/ca.key -CAcreateserial -out certs/server.pem -days 825 -sha256 -extfile <( +cat <<-EOF +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = backend +IP.1 = 127.0.0.1 +IP.2 = 0.0.0.0 +EOF +) + +# Generate Client Certificate +openssl genrsa -out certs/client.key 4096 +openssl req -new -key certs/client.key -out certs/client.csr -config <( +cat <<-EOF +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +distinguished_name = dn + +[dn] +CN = loki-backoffice +EOF +) + +openssl x509 -req -in certs/client.csr -CA certs/ca.pem -CAkey certs/ca.key -CAcreateserial -out certs/client.pem -days 825 -sha256 +``` + +For more detailed information on certificates, see [Certificates Documentation](docs/certificates.md). + +### Database Migrations Run the following command to apply database migrations: @@ -45,17 +120,21 @@ Run the following command to apply database migrations: GO_ENV=development make db:drop db:create db:migrate ``` -**Run the Services**: +### Run application ```sh docker-compose build docker-compose up ``` -**Check health status**: +### Check health status ```sh -curl -X GET http://localhost:8080/health +curl -X GET http://localhost:8080/live +``` + +```sh +curl -X GET http://localhost:8080/ready ``` ## Documentation @@ -66,6 +145,15 @@ curl -X GET http://localhost:8080/health Swagger file is available at [api/swagger.yaml](https://github.com/tab/loki/blob/master/api/swagger.yaml) +## Related Repositories + +- [Loki Infrastructure](https://github.com/tab/loki-infrastructure) - Infrastructure setup for the Loki ecosystem +- [Loki Backoffice](https://github.com/tab/loki-backoffice) - Backoffice service +- [Loki Proto](https://github.com/tab/loki-proto) - Protocol buffer definitions +- [Loki Frontend](https://github.com/tab/loki-frontend) - Frontend application +- [Smart-ID Client](https://github.com/tab/smartid) - Smart-ID client used for authentication +- [Mobile-ID Client](https://github.com/tab/mobileid) - Mobile-ID client used for authentication + ## Architecture The application follows a layered architecture and clean code principles: diff --git a/docs/index.md b/docs/index.md index 0740ae8..e881670 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,6 +13,7 @@ Designed to be easily integrated into microservices architectures and provides l ## Contents +- [Repositories](repositories.md) - [Installation](installation.md) - [Certificates](certificates.md) - [Usage](usage.md) diff --git a/docs/installation.md b/docs/installation.md index 3621b13..02f4712 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,14 +1,42 @@ # Installation -**Environment Variables**: +## Prerequisites + +Before starting the Loki application, you must have the loki-infrastructure running: + +```sh +git clone git@github.com/tab/loki-infrastructure.git +cd loki-infrastructure + +docker-compose up +``` + +## Environment Variables Use `.env` files (e.g., `.env.development`) or provide environment variables for: - `DATABASE_DSN` for PostgreSQL - `REDIS_URI` for Redis - `SMART_ID_API_URL`, `MOBILE_ID_API_URL` and corresponding relying on party credentials +- `TELEMETRY_URI` for OpenTelemetry + +Example `.env.development` file: + +``` +DATABASE_DSN=postgres://postgres:postgres@localhost:5432/loki_development?sslmode=disable +REDIS_URI=redis://localhost:6379/0 +SMART_ID_API_URL=https://sid.demo.sk.ee/smart-id-rp/v2/ +MOBILE_ID_API_URL=https://tsp.demo.sk.ee/mid-api/ +TELEMETRY_URI=http://localhost:4317 +``` + +## Certificate and Key Generation + +Before running the services, you need to generate certificates for mTLS and keys for JWT signing. -**Database Migrations**: +For more detailed information on certificates, see [Certificates Documentation](certificates.md). + +## Database Migrations Run the following command to apply database migrations: @@ -16,15 +44,30 @@ Run the following command to apply database migrations: GO_ENV=development make db:drop db:create db:migrate ``` -**Run the Services**: +## Run application ```sh docker-compose build docker-compose up ``` -**Check health status**: +### Check health status + +```sh +curl -X GET http://localhost:8080/live +``` ```sh -curl -X GET http://localhost:8080/health +curl -X GET http://localhost:8080/ready ``` + +## Related Repositories + +The Loki ecosystem consists of the following repositories: + +- [Loki Infrastructure](https://github.com/tab/loki-infrastructure) - Infrastructure setup for the Loki ecosystem +- [Loki Backoffice](https://github.com/tab/loki-backoffice) - Backoffice service +- [Loki Proto](https://github.com/tab/loki-proto) - Protocol buffer definitions +- [Loki Frontend](https://github.com/tab/loki-frontend) - Frontend application +- [Smart-ID Client](https://github.com/tab/smartid) - Smart-ID client used for authentication +- [Mobile-ID Client](https://github.com/tab/mobileid) - Mobile-ID client used for authentication diff --git a/docs/repositories.md b/docs/repositories.md new file mode 100644 index 0000000..82b163b --- /dev/null +++ b/docs/repositories.md @@ -0,0 +1,10 @@ +# Related repositories + +The Loki ecosystem consists of the following repositories: + +- [Loki Infrastructure](https://github.com/tab/loki-infrastructure) - Infrastructure setup for the Loki ecosystem +- [Loki Backoffice](https://github.com/tab/loki-backoffice) - Backoffice service +- [Loki Proto](https://github.com/tab/loki-proto) - Protocol buffer definitions +- [Loki Frontend](https://github.com/tab/loki-frontend) - Frontend application +- [Smart-ID Client](https://github.com/tab/smartid) - Smart-ID client used for authentication +- [Mobile-ID Client](https://github.com/tab/mobileid) - Mobile-ID client used for authentication diff --git a/docs/usage.md b/docs/usage.md index 5c01af7..417d913 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -24,6 +24,7 @@ example: ```sh curl -X POST http://localhost:8080/api/auth/smart_id \ -H "Content-Type: application/json" \ + -H "X-Request-ID: 4de2f35d-7e30-466e-923b-aab80a424b34" \ -H "X-Trace-ID: f4c28fec-07fd-415f-900c-37be7fb705fa" \ -d '{ "country": "EE", "personal_code": "50001029996" }' ``` @@ -44,6 +45,7 @@ example: ```sh curl -X GET http://localhost:8080/api/sessions/a658556f-f2ec-42f5-86dc-2665f011d5f7 \ -H "Content-Type: application/json" \ + -H "X-Request-ID: 7877796b-54b9-4737-a44f-0b0bb4f5eb88" \ -H "X-Trace-ID: f4c28fec-07fd-415f-900c-37be7fb705fa" ``` @@ -63,6 +65,7 @@ example: ```sh curl -X POST http://localhost:8080/api/sessions/a658556f-f2ec-42f5-86dc-2665f011d5f7 \ -H "Content-Type: application/json" \ + -H "X-Request-ID: 2aeb8bca-8af0-498f-8136-c179d3a6f1bd" \ -H "X-Trace-ID: f4c28fec-07fd-415f-900c-37be7fb705fa" ``` @@ -142,6 +145,7 @@ example: curl -X GET http://localhost:8080/api/me \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ + -H "X-Request-ID: 4844c624-4c3f-4cdf-96dd-01bc53915e02" \ -H "X-Trace-ID: 0cbc1fe0-c29c-44d5-84a1-4ec5ddb9e08f" ``` response: @@ -180,6 +184,7 @@ example: ```sh curl -X POST http://localhost:8080/api/tokens/refresh \ -H "Content-Type: application/json" \ + -H "X-Request-ID: 83bc86e9-1a29-46a8-b358-6db39ab7c2f5" \ -H "X-Trace-ID: 754cfd21-69b2-436a-af5f-737932cfd874" -d '{ "refresh_token": "" }' ``` From ecbdc58a076ea60fdf78c8e750ec0ac2d7d1dcf6 Mon Sep 17 00:00:00 2001 From: tab Date: Sat, 12 Apr 2025 14:43:53 +0300 Subject: [PATCH 18/20] feat(ci): Add integration testing framework and workflow --- .github/actions/integration/Makefile | 145 ++++++ .github/actions/integration/auth.lua | 59 +++ .github/actions/integration/framework.lua | 452 ++++++++++++++++++ .github/actions/integration/generate-certs.sh | 102 ++++ .../loki-backoffice-compose.override.yaml | 14 + .../integration/loki-compose.override.yaml | 16 + .../actions/integration/resources/me_spec.lua | 137 ++++++ .../integration/resources/mobile_id_spec.lua | 131 +++++ .../integration/resources/smart_id_spec.lua | 131 +++++ .github/actions/integration/run.lua | 117 +++++ .github/workflows/checks.yaml | 6 + .github/workflows/integration.yaml | 170 +++++++ 12 files changed, 1480 insertions(+) create mode 100644 .github/actions/integration/Makefile create mode 100644 .github/actions/integration/auth.lua create mode 100644 .github/actions/integration/framework.lua create mode 100755 .github/actions/integration/generate-certs.sh create mode 100644 .github/actions/integration/loki-backoffice-compose.override.yaml create mode 100644 .github/actions/integration/loki-compose.override.yaml create mode 100644 .github/actions/integration/resources/me_spec.lua create mode 100644 .github/actions/integration/resources/mobile_id_spec.lua create mode 100644 .github/actions/integration/resources/smart_id_spec.lua create mode 100644 .github/actions/integration/run.lua create mode 100644 .github/workflows/integration.yaml diff --git a/.github/actions/integration/Makefile b/.github/actions/integration/Makefile new file mode 100644 index 0000000..b16c017 --- /dev/null +++ b/.github/actions/integration/Makefile @@ -0,0 +1,145 @@ +LOKI_REPO ?= $(shell pwd)/../../loki +LOKI_BACKOFFICE_REPO ?= $(shell pwd)/../../loki-backoffice + +LOKI_DB_NAME = loki-test +BACKOFFICE_DB_NAME = loki-backoffice-test +DB_USER = postgres +DB_PASSWORD = postgres +DB_HOST = localhost +DB_PORT = 5432 + +GOOSE_DRIVER = postgres +LOKI_GOOSE_MIGRATION_DIR = $(LOKI_REPO)/db/migrate +BACKOFFICE_GOOSE_MIGRATION_DIR = $(LOKI_BACKOFFICE_REPO)/db/migrate + +NETWORK_NAME = loki-network + +ifneq (,$(wildcard $(LOKI_REPO)/.env.test)) + include $(LOKI_REPO)/.env.test + export $(shell sed 's/=.*//' $(LOKI_REPO)/.env.test) +endif + +ifneq (,$(wildcard $(LOKI_BACKOFFICE_REPO)/.env.test)) + include $(LOKI_BACKOFFICE_REPO)/.env.test + export $(shell sed 's/=.*//' $(LOKI_BACKOFFICE_REPO)/.env.test) +endif + +.PHONY: setup +setup: db\:setup certs\:generate docker\:network docker\:start check\:services + +.PHONY: db\:setup +db\:setup: db\:create db\:migrate + +.PHONY: db\:create +db\:create: + @echo "Creating databases for integration tests..." + @echo "PostgreSQL: $(DB_HOST):$(DB_PORT)" + PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -U $(DB_USER) -c "DROP DATABASE IF EXISTS \"$(LOKI_DB_NAME)\";" postgres + PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -U $(DB_USER) -c "CREATE DATABASE \"$(LOKI_DB_NAME)\";" postgres + PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -U $(DB_USER) -c "DROP DATABASE IF EXISTS \"$(BACKOFFICE_DB_NAME)\";" postgres + PGPASSWORD=$(DB_PASSWORD) psql -h $(DB_HOST) -U $(DB_USER) -c "CREATE DATABASE \"$(BACKOFFICE_DB_NAME)\";" postgres + @echo "Databases created successfully" + +.PHONY: db\:migrate +db\:migrate: + @echo "Running migrations..." + @if [ -d "$(LOKI_GOOSE_MIGRATION_DIR)" ]; then \ + echo "Running loki migrations..."; \ + GOOSE_DRIVER=$(GOOSE_DRIVER) GOOSE_DBSTRING="host=$(DB_HOST) port=$(DB_PORT) user=$(DB_USER) password=$(DB_PASSWORD) dbname=$(LOKI_DB_NAME) sslmode=disable" goose -dir $(LOKI_GOOSE_MIGRATION_DIR) up || echo "Note: Some loki migrations might fail if tables already exist from schema"; \ + else \ + echo "Warning: Loki migrations directory not found at $(LOKI_GOOSE_MIGRATION_DIR)"; \ + fi + + @if [ -d "$(BACKOFFICE_GOOSE_MIGRATION_DIR)" ]; then \ + echo "Running loki-backoffice migrations..."; \ + GOOSE_DRIVER=$(GOOSE_DRIVER) GOOSE_DBSTRING="host=$(DB_HOST) port=$(DB_PORT) user=$(DB_USER) password=$(DB_PASSWORD) dbname=$(BACKOFFICE_DB_NAME) sslmode=disable" goose -dir $(BACKOFFICE_GOOSE_MIGRATION_DIR) up || echo "Note: Some loki-backoffice migrations might fail if tables already exist from schema"; \ + else \ + echo "Warning: Loki-backoffice migrations directory not found at $(BACKOFFICE_GOOSE_MIGRATION_DIR)"; \ + fi + @echo "Migrations completed" + +.PHONY: certs\:generate +certs\:generate: + @echo "Generating JWT keys and mTLS certificates..." + ./generate-certs.sh "$(LOKI_REPO)" "$(LOKI_BACKOFFICE_REPO)" + @echo "Certificate generation completed successfully" + +.PHONY: docker\:network +docker\:network: + @echo "Creating Docker network..." + docker network inspect $(NETWORK_NAME) >/dev/null 2>&1 || docker network create $(NETWORK_NAME) + @echo "Docker network ready" + +.PHONY: docker\:start +docker\:start: + @echo "Starting services..." + cp loki-compose.override.yaml $(LOKI_REPO)/compose.override.yaml + cp loki-backoffice-compose.override.yaml $(LOKI_BACKOFFICE_REPO)/compose.override.yaml + + cd $(LOKI_REPO) && docker compose up -d + cd $(LOKI_BACKOFFICE_REPO) && docker compose up -d + + @echo "Services started" + +.PHONY: check\:services +check\:services: + @echo "Waiting for services to be ready..." + @echo "Displaying initial container logs to help with debugging..." + @echo "Loki logs:" && docker logs loki + @echo "Loki-backoffice logs:" && docker logs loki-backoffice + + @echo "Testing connection to services..." + @for i in $$(seq 1 5); do \ + echo "Attempt $$i/5:"; \ + if curl -s --max-time 5 http://localhost:8080/live 2>&1 | grep -q "alive"; then \ + echo "✅ Loki service is up"; \ + LOKI_UP=1; \ + else \ + echo "❌ Loki service not responding yet"; \ + LOKI_UP=0; \ + docker logs --tail 20 loki; \ + fi; \ + if curl -s --max-time 5 http://localhost:8081/live 2>&1 | grep -q "alive"; then \ + echo "✅ Loki-backoffice service is up"; \ + BACKOFFICE_UP=1; \ + else \ + echo "❌ Loki-backoffice service not responding yet"; \ + BACKOFFICE_UP=0; \ + docker logs --tail 20 loki-backoffice; \ + fi; \ + if [ "$$LOKI_UP" = "1" ] && [ "$$BACKOFFICE_UP" = "1" ]; then \ + break; \ + fi; \ + if [ $$i -eq 5 ]; then \ + echo "⚠️ Timed out waiting for services"; \ + echo "Full Loki logs:"; \ + docker logs loki; \ + echo "Full Loki-backoffice logs:"; \ + docker logs loki-backoffice; \ + exit 1; \ + fi; \ + echo "Waiting for services to start (attempt $$i/5)... retrying in 3 seconds"; \ + sleep 3; \ + done + + @echo "All services are ready!" + +.PHONY: run +run: + @echo "Running integration tests..." + lua run.lua + +.PHONY: cleanup +cleanup: + @echo "Cleaning up..." + cd $(LOKI_REPO) && docker compose down || true + cd $(LOKI_BACKOFFICE_REPO) && docker compose down || true + + rm -f $(LOKI_REPO)/compose.override.yaml + rm -f $(LOKI_BACKOFFICE_REPO)/compose.override.yaml + + docker network rm $(NETWORK_NAME) || true + @echo "Cleanup complete" + +.PHONY: all +all: setup run cleanup diff --git a/.github/actions/integration/auth.lua b/.github/actions/integration/auth.lua new file mode 100644 index 0000000..590a40d --- /dev/null +++ b/.github/actions/integration/auth.lua @@ -0,0 +1,59 @@ +local framework = require("framework") +local auth = {} + +local token_cache = { + admin = nil, + manager = nil, + user = nil +} + +function auth.get_admin_token() + if token_cache.admin then + print("Using cached admin token") + return token_cache.admin + end + + local token = framework.authenticate_with_smart_id("EE", "40504040001") + if not token then + error("Failed to get admin token") + end + + token_cache.admin = token + return token +end + +function auth.get_manager_token() + if token_cache.manager then + print("Using cached manager token") + return token_cache.manager + end + + local token = framework.authenticate_with_smart_id("BE", "00010299944") + if not token then + error("Failed to get manager token") + end + + token_cache.manager = token + return token +end + +function auth.get_user_token() + if token_cache.user then + print("Using cached user token") + return token_cache.user + end + + local token = framework.authenticate_with_smart_id("EE", "30303039914") + if not token then + error("Failed to get user token") + end + + token_cache.user = token + return token +end + +function auth.get_invalid_token() + return "invalid-token" +end + +return auth diff --git a/.github/actions/integration/framework.lua b/.github/actions/integration/framework.lua new file mode 100644 index 0000000..de6fa57 --- /dev/null +++ b/.github/actions/integration/framework.lua @@ -0,0 +1,452 @@ +local http = require("socket.http") +local ltn12 = require("ltn12") +local json = require("cjson") +local uuid = require("uuid") + +uuid.randomseed(os.time()) + +local function rng() + local bytes = {} + for i = 1, 16 do + bytes[i] = math.random(0, 255) + end + return string.char(table.unpack(bytes)) +end + +uuid.set_rng(rng) + +local framework = { + _debug = true +} + +framework.config = { + loki_url = "http://localhost:8080", + backoffice_url = "http://localhost:8081", + timeout = 60 +} + +function framework.debug(enabled) + framework._debug = enabled +end + +function framework.generate_trace_id() + return uuid() +end + +function framework.generate_request_id() + return uuid() +end + +function framework.log_debug(...) + if framework._debug then + print("[DEBUG]", ...) + end +end + +function framework.request(method, url, headers, body) + local response_body = {} + local request_body = nil + + if body then + request_body = json.encode(body) + framework.log_debug("Request body:", request_body) + end + + headers = headers or {} + headers["Content-Type"] = headers["Content-Type"] or "application/json" + + framework.log_debug(string.format("Making %s request to %s", method, url)) + framework.log_debug("Headers:", json.encode(headers)) + + local response, status_code, response_headers = http.request { + url = url, + method = method, + headers = headers, + source = request_body and ltn12.source.string(request_body) or nil, + sink = ltn12.sink.table(response_body), + timeout = framework.config.timeout + } + + local body_str = table.concat(response_body) + local response_data = nil + + if body_str and body_str ~= "" then + pcall(function() + response_data = json.decode(body_str) + end) + end + + framework.log_debug(string.format("Response status: %s", status_code)) + framework.log_debug("Response body:", body_str) + + return { + status = status_code, + headers = response_headers, + body = response_data, + raw_body = body_str + } +end + +function framework.loki_readiness() + local trace_id = framework.generate_trace_id() + local request_id = framework.generate_request_id() + + local response = framework.request( + "GET", + framework.config.loki_url .. "/ready", + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + } + ) + + if not response.body then + print("Failed to get loki liveness status") + return nil + end + + return response +end + +function framework.loki_backoffice_readiness() + local trace_id = framework.generate_trace_id() + local request_id = framework.generate_request_id() + + local response = framework.request( + "GET", + framework.config.backoffice_url .. "/ready", + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + } + ) + + if not response.body then + print("Failed to get loki-backoffice liveness status") + return nil + end + + return response +end + +function framework.start_smart_id_auth(country, personal_code) + local trace_id = framework.generate_trace_id() + local request_id = framework.generate_request_id() + + local response = framework.request( + "POST", + framework.config.loki_url .. "/api/auth/smart_id", + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + }, + {country = country, personal_code = personal_code} + ) + + if not response.body or not response.body.id then + print("Failed to create session") + print("Response:", json.encode(response)) + return nil + end + + print("Session ID: " .. response.body.id) + if response.body.code then + print("Verification code: " .. response.body.code) + end + + return response.body.id, trace_id +end + +function framework.start_mobile_id_auth(phone_number, personal_code) + local trace_id = framework.generate_trace_id() + local request_id = framework.generate_request_id() + + local response = framework.request( + "POST", + framework.config.loki_url .. "/api/auth/mobile_id", + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + }, + { + phone_number = phone_number, + personal_code = personal_code + } + ) + + if not response.body or not response.body.id then + print("Failed to create session") + print("Response:", json.encode(response)) + return nil + end + + print("Session ID: " .. response.body.id) + if response.body.code then + print("Verification code: " .. response.body.code) + end + + return response.body.id, trace_id +end + +function framework.check_session_status(session_id, trace_id) + local request_id = framework.generate_request_id() + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/sessions/" .. session_id, + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + } + ) + + if not response.body then + print("Failed to get session status") + return nil + end + + return response.body.status, response.body.error +end + +function framework.check_error_type(session_id, trace_id, expected_error) + local request_id = framework.generate_request_id() + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/sessions/" .. session_id, + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + } + ) + + if not response.body then + print("Failed to get session status") + return false + end + + print("Session status: " .. (response.body.status or "unknown")) + if response.body.error then + print("Error: " .. response.body.error) + return string.find(response.body.error, expected_error, 1, true) ~= nil + end + + return false +end + +function framework.wait_for_session_completion(session_id, trace_id, max_attempts, expected_error) + max_attempts = max_attempts or 10 + + for i = 1, max_attempts do + local status, error_type = framework.check_session_status(session_id, trace_id) + print("Session status: " .. (status or "unknown")) + + if error_type then + print("Session error: " .. error_type) + end + + if status == "SUCCESS" then + return true + end + + if status == "ERROR" and expected_error then + if error_type and string.find(error_type, expected_error, 1, true) then + print("Received expected error (contained in error message): " .. error_type) + return true + else + print("ERROR status received but expected error not found in message: " .. (error_type or "nil") .. ", expected to contain: " .. expected_error) + end + end + + if i == max_attempts then + print("Timed out waiting for session completion") + if expected_error then + local has_expected = framework.check_error_type(session_id, trace_id, expected_error) + if has_expected then + print("Found expected error on last attempt: " .. expected_error) + return true + end + end + return false + end + + os.execute("sleep 3") + end + + return false +end + +function framework.complete_auth(session_id, trace_id) + local request_id = framework.generate_request_id() + + local response = framework.request( + "POST", + framework.config.loki_url .. "/api/sessions/" .. session_id, + { + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + } + ) + + if not response.body or not response.body.access_token then + print("Failed to get access token") + print("Response:", json.encode(response)) + return nil + end + + if not response.body or not response.body.refresh_token then + print("Failed to get refresh token") + print("Response:", json.encode(response)) + return nil + end + + print("Access token received") + return response.body.access_token +end + +function framework.call_backoffice_api(endpoint, token) + local trace_id = framework.generate_trace_id() + local request_id = framework.generate_request_id() + + local response = framework.request( + "GET", + framework.config.backoffice_url .. endpoint, + { + ["Authorization"] = "Bearer " .. token, + ["X-Trace-ID"] = trace_id, + ["X-Request-ID"] = request_id + } + ) + + return response +end + +function framework.authenticate_with_smart_id(country, personal_code, expected_error) + print(string.format("Authenticating with Smart-ID (Country: %s, Personal Code: %s)", country, personal_code)) + + local session_id, trace_id = framework.start_smart_id_auth(country, personal_code) + if not session_id then + print("Failed to start Smart-ID authentication") + return nil + end + + if expected_error then + print("Expecting error: " .. expected_error) + if not framework.wait_for_session_completion(session_id, trace_id, 15, expected_error) then + if framework.check_error_type(session_id, trace_id, expected_error) then + print("Found expected error after completion failed: " .. expected_error) + return true + end + print("Failed waiting for expected error: " .. expected_error) + return nil + end + + return true + else + if not framework.wait_for_session_completion(session_id, trace_id) then + print("Failed waiting for session completion") + return nil + end + + local token = framework.complete_auth(session_id, trace_id) + if not token then + print("Failed to complete authentication") + return nil + end + + print("Authentication successful") + return token + end +end + +function framework.authenticate_with_mobile_id(phone_number, personal_code, expected_error) + print(string.format("Authenticating with Mobile-ID (Phone Number: %s, Personal Code: %s)", phone_number, personal_code)) + + local session_id, trace_id = framework.start_mobile_id_auth(phone_number, personal_code) + if not session_id then + print("Failed to start Mobile-ID authentication") + return nil + end + + if expected_error then + print("Expecting error: " .. expected_error) + if not framework.wait_for_session_completion(session_id, trace_id, 15, expected_error) then + if framework.check_error_type(session_id, trace_id, expected_error) then + print("Found expected error after completion failed: " .. expected_error) + return true + end + print("Failed waiting for expected error: " .. expected_error) + return nil + end + + return true + else + if not framework.wait_for_session_completion(session_id, trace_id) then + print("Failed waiting for session completion") + return nil + end + + local token = framework.complete_auth(session_id, trace_id) + if not token then + print("Failed to complete authentication") + return nil + end + + print("Authentication successful") + return token + end +end + +framework.assert = {} + +function framework.assert.equals(expected, actual, message) + if expected ~= actual then + error(string.format("%s: Expected %s but got %s", + message or "Assertion failed", + tostring(expected), + tostring(actual) + )) + end + return true +end + +function framework.assert.not_equals(expected, actual, message) + if expected == actual then + error(string.format("%s: Expected value to not equal %s", + message or "Assertion failed", + tostring(expected) + )) + end + return true +end + +function framework.assert.contains(haystack, needle, message) + if type(haystack) ~= "string" or type(needle) ~= "string" or not string.find(haystack, needle, 1, true) then + error(string.format("%s: Expected '%s' to contain '%s'", + message or "Assertion failed", + tostring(haystack), + tostring(needle) + )) + end + return true +end + +function framework.assert.status_code(response, expected_status) + return framework.assert.equals( + expected_status, + response.status, + "Unexpected status code" + ) +end + +function framework.assert.has_property(obj, property, message) + if type(obj) ~= "table" or obj[property] == nil then + error(string.format("%s: Expected object to have property '%s'", + message or "Assertion failed", + tostring(property) + )) + end + return true +end + +return framework diff --git a/.github/actions/integration/generate-certs.sh b/.github/actions/integration/generate-certs.sh new file mode 100755 index 0000000..96fa485 --- /dev/null +++ b/.github/actions/integration/generate-certs.sh @@ -0,0 +1,102 @@ +#!/bin/bash +set -e + +# This script generates JWT keys and mTLS certificates for Loki and Loki-backoffice +# Usage: ./generate-certs.sh [LOKI_REPO] [LOKI_BACKOFFICE_REPO] + +LOKI_REPO="${1:-../../loki}" +LOKI_BACKOFFICE_REPO="${2:-../../loki-backoffice}" + +echo "Loki repo: $LOKI_REPO" +echo "Loki-backoffice repo: $LOKI_BACKOFFICE_REPO" + +mkdir -p "$LOKI_REPO/certs/jwt" +mkdir -p "$LOKI_BACKOFFICE_REPO/certs/jwt" + +echo "Generating JWT signing keys..." +openssl genrsa -out "$LOKI_REPO/certs/jwt/private.key" 4096 +openssl rsa -in "$LOKI_REPO/certs/jwt/private.key" -pubout -out "$LOKI_REPO/certs/jwt/public.key" +cp "$LOKI_REPO/certs/jwt/public.key" "$LOKI_BACKOFFICE_REPO/certs/jwt/public.key" + +echo "Generating Certificate Authority (CA)..." +openssl genrsa -out "$LOKI_REPO/certs/ca.key" 4096 +openssl req -new -x509 -key "$LOKI_REPO/certs/ca.key" -sha256 -subj "/CN=Loki CA" \ + -out "$LOKI_REPO/certs/ca.pem" -days 3650 +cp "$LOKI_REPO/certs/ca.pem" "$LOKI_BACKOFFICE_REPO/certs/ca.pem" + +echo "Creating temporary config files..." +cat > server_config.cnf << EOF +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +req_extensions = req_ext +distinguished_name = dn + +[dn] +CN = loki-backend + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = backend +DNS.3 = loki +DNS.4 = loki-backend +IP.1 = 127.0.0.1 +IP.2 = 0.0.0.0 +EOF + +cat > server_ext.cnf << EOF +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = backend +DNS.3 = loki +DNS.4 = loki-backend +IP.1 = 127.0.0.1 +IP.2 = 0.0.0.0 +EOF + +cat > client_config.cnf << EOF +[req] +default_bits = 4096 +prompt = no +default_md = sha256 +distinguished_name = dn + +[dn] +CN = loki-backoffice +EOF + +echo "Generating Server Certificate..." +openssl genrsa -out "$LOKI_REPO/certs/server.key" 4096 +openssl req -new -key "$LOKI_REPO/certs/server.key" \ + -out "$LOKI_REPO/certs/server.csr" -config server_config.cnf +openssl x509 -req -in "$LOKI_REPO/certs/server.csr" -CA "$LOKI_REPO/certs/ca.pem" \ + -CAkey "$LOKI_REPO/certs/ca.key" -CAcreateserial \ + -out "$LOKI_REPO/certs/server.pem" -days 825 -sha256 -extfile server_ext.cnf + +echo "Generating Client Certificate..." +openssl genrsa -out "$LOKI_BACKOFFICE_REPO/certs/client.key" 4096 +openssl req -new -key "$LOKI_BACKOFFICE_REPO/certs/client.key" \ + -out "$LOKI_BACKOFFICE_REPO/certs/client.csr" -config client_config.cnf +openssl x509 -req -in "$LOKI_BACKOFFICE_REPO/certs/client.csr" -CA "$LOKI_REPO/certs/ca.pem" \ + -CAkey "$LOKI_REPO/certs/ca.key" -CAcreateserial \ + -out "$LOKI_BACKOFFICE_REPO/certs/client.pem" -days 825 -sha256 + +echo "Verifying certificates..." +openssl verify -CAfile "$LOKI_REPO/certs/ca.pem" "$LOKI_REPO/certs/server.pem" +openssl verify -CAfile "$LOKI_REPO/certs/ca.pem" "$LOKI_BACKOFFICE_REPO/certs/client.pem" + +rm -f server_config.cnf server_ext.cnf client_config.cnf + +echo "Generated JWT keys and certificates:" +echo "Loki certificates:" +ls -la "$LOKI_REPO/certs" +ls -la "$LOKI_REPO/certs/jwt" +echo "Loki-backoffice certificates:" +ls -la "$LOKI_BACKOFFICE_REPO/certs" +ls -la "$LOKI_BACKOFFICE_REPO/certs/jwt" diff --git a/.github/actions/integration/loki-backoffice-compose.override.yaml b/.github/actions/integration/loki-backoffice-compose.override.yaml new file mode 100644 index 0000000..dbe78ac --- /dev/null +++ b/.github/actions/integration/loki-backoffice-compose.override.yaml @@ -0,0 +1,14 @@ +services: + backoffice: + environment: + - APP_NAME=loki-backoffice + - APP_ADDRESS=0.0.0.0:8081 + - GRPC_ADDRESS=loki:50051 + - CERT_PATH=/run/certs + - DATABASE_DSN=postgres://postgres:postgres@postgres:5432/loki-backoffice-test?sslmode=disable + - LOG_LEVEL=debug + container_name: loki-backoffice + networks: + - loki-network + extra_hosts: + - "postgres:host-gateway" diff --git a/.github/actions/integration/loki-compose.override.yaml b/.github/actions/integration/loki-compose.override.yaml new file mode 100644 index 0000000..a3696ee --- /dev/null +++ b/.github/actions/integration/loki-compose.override.yaml @@ -0,0 +1,16 @@ +services: + backend: + environment: + - APP_NAME=loki + - APP_ADDRESS=0.0.0.0:8080 + - GRPC_ADDRESS=0.0.0.0:50051 + - CERT_PATH=/run/certs + - DATABASE_DSN=postgres://postgres:postgres@postgres:5432/loki-test?sslmode=disable + - REDIS_URI=redis://redis:6379/1 + - LOG_LEVEL=debug + container_name: loki + networks: + - loki-network + extra_hosts: + - "postgres:host-gateway" + - "redis:host-gateway" diff --git a/.github/actions/integration/resources/me_spec.lua b/.github/actions/integration/resources/me_spec.lua new file mode 100644 index 0000000..95d9e88 --- /dev/null +++ b/.github/actions/integration/resources/me_spec.lua @@ -0,0 +1,137 @@ +local framework = require("framework") +local auth = require("auth") +local json = require("cjson") + +local suite = {} + +local test_data = { + admin_token = nil, + manager_token = nil, + user_token = nil, +} + +function suite.setup() + print("Setting up tests...") + test_data.admin_token = auth.get_admin_token() + test_data.manager_token = auth.get_manager_token() + test_data.user_token = auth.get_user_token() + + return test_data.admin_token ~= nil +end + +function suite.test_get_admin() + print("Test: Get me with admin token") + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/me", + { + ["Authorization"] = "Bearer " .. test_data.admin_token, + ["X-Trace-ID"] = framework.generate_trace_id(), + ["X-Request-ID"] = framework.generate_request_id() + } + ) + framework.assert.status_code(response, 200) + + print("Test: Get me with user token") + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/me", + { + ["Authorization"] = "Bearer " .. test_data.user_token, + ["X-Trace-ID"] = framework.generate_trace_id(), + ["X-Request-ID"] = framework.generate_request_id() + } + ) + + framework.assert.status_code(response, 200) + + return true +end + +function suite.test_get_manager() + print("Test: Get me with manager token") + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/me", + { + ["Authorization"] = "Bearer " .. test_data.manager_token, + ["X-Trace-ID"] = framework.generate_trace_id(), + ["X-Request-ID"] = framework.generate_request_id() + } + ) + + framework.assert.status_code(response, 200) + + return true +end + +function suite.test_get_user() + print("Test: Get me with user token") + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/me", + { + ["Authorization"] = "Bearer " .. test_data.user_token, + ["X-Trace-ID"] = framework.generate_trace_id(), + ["X-Request-ID"] = framework.generate_request_id() + } + ) + + framework.assert.status_code(response, 200) + + return true +end + +function suite.test_unauthorized() + print("Test: Unauthorized access") + + local response = framework.request( + "GET", + framework.config.loki_url .. "/api/me", + { + ["Authorization"] = "Bearer " .. auth.get_invalid_token(), + ["X-Trace-ID"] = framework.generate_trace_id(), + ["X-Request-ID"] = framework.generate_request_id() + } + ) + + framework.assert.status_code(response, 401) + + return true +end + +function suite.run() + if not suite.setup() then + print("❌ Setup failed") + return false + end + + local tests = { + suite.test_get_admin, + suite.test_get_manager, + suite.test_get_user, + suite.test_unauthorized + } + + local success = true + + for i, test in ipairs(tests) do + local test_success, result = pcall(test) + + if not test_success or not result then + print("❌ Test failed: " .. debug.traceback()) + success = false + break + else + print("✅ Test passed") + end + end + + return success +end + +return suite diff --git a/.github/actions/integration/resources/mobile_id_spec.lua b/.github/actions/integration/resources/mobile_id_spec.lua new file mode 100644 index 0000000..ae3b89a --- /dev/null +++ b/.github/actions/integration/resources/mobile_id_spec.lua @@ -0,0 +1,131 @@ +local framework = require("framework") +local json = require("cjson") + +local suite = {} + +local test_data = { + success_user = { phone_number = "+37268000769", personal_code = "60001017869" }, + not_mid_client = { phone_number = "+37200000266", personal_code = "60001019939" }, + delivery_error = { phone_number = "+37207110066", personal_code = "60001019947" }, + user_cancelled = { phone_number = "+37201100266", personal_code = "60001019950" }, + signature_hash_mismatch = { phone_number = "+37200000666", personal_code = "60001019961" }, + sim_error = { phone_number = "+37201200266", personal_code = "60001019972" }, + phone_absent = { phone_number = "+37213100266", personal_code = "60001019983" }, + timeout = { phone_number = "+37266000266", personal_code = "50001018908" } +} + +function suite.test_success_authentication() + local user = test_data.success_user + print(string.format("Testing successful authentication with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local token = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code) + if not token then + error("Failed to authenticate with valid credentials") + end + + framework.assert.not_equals(nil, token, "Token should not be nil") + framework.assert.not_equals("", token, "Token should not be empty") + + return token +end + +function suite.test_not_mid_client() + local user = test_data.not_mid_client + print(string.format("Testing NOT_MID_CLIENT scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "NOT_MID_CLIENT") + framework.assert.equals(true, result, "Authentication with expected NOT_MID_CLIENT error should succeed") + + return true +end + +function suite.test_delivery_error() + local user = test_data.delivery_error + print(string.format("Testing DELIVERY_ERROR scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "DELIVERY_ERROR") + framework.assert.equals(true, result, "Authentication with expected DELIVERY_ERROR error should succeed") + + return true +end + +function suite.test_user_cancelled() + local user = test_data.user_cancelled + print(string.format("Testing USER_CANCELLED scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "USER_CANCELLED") + framework.assert.equals(true, result, "Authentication with expected USER_CANCELLED error should succeed") + + return true +end + +function suite.test_signature_hash_mismatch() + local user = test_data.signature_hash_mismatch + print(string.format("Testing SIGNATURE_HASH_MISMATCH scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "SIGNATURE_HASH_MISMATCH") + framework.assert.equals(true, result, "Authentication with expected SIGNATURE_HASH_MISMATCH error should succeed") + + return true +end + +function suite.test_sim_error() + local user = test_data.sim_error + print(string.format("Testing SIM_ERROR scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "SIM_ERROR") + framework.assert.equals(true, result, "Authentication with expected SIM_ERROR error should succeed") + + return true +end + +function suite.test_phone_absent() + local user = test_data.phone_absent + print(string.format("Testing PHONE_ABSENT scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "PHONE_ABSENT") + framework.assert.equals(true, result, "Authentication with expected PHONE_ABSENT error should succeed") + + return true +end + +function suite.test_timeout() + local user = test_data.timeout + print(string.format("Testing TIMEOUT scenario with Mobile-ID (Phone: %s, Personal Code: %s)", user.phone_number, user.personal_code)) + + local result = framework.authenticate_with_mobile_id(user.phone_number, user.personal_code, "TIMEOUT") + framework.assert.equals(true, result, "Authentication with expected TIMEOUT error should succeed") + + return true +end + +function suite.run() + local tests = { + suite.test_success_authentication, + suite.test_not_mid_client, + suite.test_delivery_error, + suite.test_user_cancelled, + suite.test_signature_hash_mismatch, + suite.test_sim_error, + suite.test_phone_absent, + suite.test_timeout + } + + local success = true + + for i, test in ipairs(tests) do + local test_success, result = pcall(test) + + if not test_success or not result then + print("❌ Test failed: " .. debug.traceback()) + success = false + break + else + print("✅ Test passed") + end + end + + return success +end + +return suite diff --git a/.github/actions/integration/resources/smart_id_spec.lua b/.github/actions/integration/resources/smart_id_spec.lua new file mode 100644 index 0000000..f90e35b --- /dev/null +++ b/.github/actions/integration/resources/smart_id_spec.lua @@ -0,0 +1,131 @@ +local framework = require("framework") +local json = require("cjson") + +local suite = {} + +local test_data = { + success_user = { country = "EE", personal_code = "40504040001" }, + user_refused = { country = "EE", personal_code = "30403039917" }, + user_refused_display_text_and_pin = { country = "EE", personal_code = "30403039928" }, + user_refused_vc_choice = { country = "EE", personal_code = "30403039939" }, + user_refused_confirmation_message = { country = "EE", personal_code = "30403039946" }, + user_refused_confirmation_message_with_vc_choice = { country = "EE", personal_code = "30403039950" }, + user_refused_cert_choice = { country = "EE", personal_code = "30403039961" }, + wrong_vc = { country = "EE", personal_code = "30403039972" } +} + +function suite.test_success_authentication() + local user = test_data.success_user + print(string.format("Testing successful authentication with Smart-ID (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local token = framework.authenticate_with_smart_id(user.country, user.personal_code) + if not token then + error("Failed to authenticate with valid credentials") + end + + framework.assert.not_equals(nil, token, "Token should not be nil") + framework.assert.not_equals("", token, "Token should not be empty") + + return token +end + +function suite.test_user_refused() + local user = test_data.user_refused + print(string.format("Testing USER_REFUSED scenario with Smart-ID (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "USER_REFUSED") + framework.assert.equals(true, result, "Authentication with expected USER_REFUSED error should succeed") + + return true +end + +function suite.test_user_refused_display_text_and_pin() + local user = test_data.user_refused_display_text_and_pin + print(string.format("Testing USER_REFUSED_DISPLAYTEXTANDPIN scenario (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "USER_REFUSED_DISPLAYTEXTANDPIN") + framework.assert.equals(true, result, "Authentication with expected USER_REFUSED_DISPLAYTEXTANDPIN error should succeed") + + return true +end + +function suite.test_user_refused_vc_choice() + local user = test_data.user_refused_vc_choice + print(string.format("Testing USER_REFUSED_VC_CHOICE scenario (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "USER_REFUSED_VC_CHOICE") + framework.assert.equals(true, result, "Authentication with expected USER_REFUSED_VC_CHOICE error should succeed") + + return true +end + +function suite.test_user_refused_confirmation_message() + local user = test_data.user_refused_confirmation_message + print(string.format("Testing USER_REFUSED_CONFIRMATIONMESSAGE scenario (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "USER_REFUSED_CONFIRMATIONMESSAGE") + framework.assert.equals(true, result, "Authentication with expected USER_REFUSED_CONFIRMATIONMESSAGE error should succeed") + + return true +end + +function suite.test_user_refused_confirmation_message_with_vc_choice() + local user = test_data.user_refused_confirmation_message_with_vc_choice + print(string.format("Testing USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE scenario (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE") + framework.assert.equals(true, result, "Authentication with expected USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE error should succeed") + + return true +end + +function suite.test_user_refused_cert_choice() + local user = test_data.user_refused_cert_choice + print(string.format("Testing USER_REFUSED_CERT_CHOICE scenario (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "USER_REFUSED_CERT_CHOICE") + framework.assert.equals(true, result, "Authentication with expected USER_REFUSED_CERT_CHOICE error should succeed") + + return true +end + +function suite.test_wrong_vc() + local user = test_data.wrong_vc + print(string.format("Testing WRONG_VC scenario (Country: %s, Personal Code: %s)", user.country, user.personal_code)) + + local result = framework.authenticate_with_smart_id(user.country, user.personal_code, "WRONG_VC") + framework.assert.equals(true, result, "Authentication with expected WRONG_VC error should succeed") + + return true +end + +function suite.run() + local tests = { + suite.test_success_authentication, + suite.test_user_refused, + suite.test_user_refused_display_text_and_pin, + suite.test_user_refused_vc_choice, + suite.test_user_refused_confirmation_message, + suite.test_user_refused_confirmation_message_with_vc_choice, + suite.test_user_refused_cert_choice, + suite.test_wrong_vc + } + + local success = true + + for i, test in ipairs(tests) do + local test_success, result = pcall(test) + + if not test_success or not result then + print("❌ Test failed: " .. debug.traceback()) + success = false + break + else + print("✅ Test passed") + end + end + + return success +end + +return suite diff --git a/.github/actions/integration/run.lua b/.github/actions/integration/run.lua new file mode 100644 index 0000000..53f6abe --- /dev/null +++ b/.github/actions/integration/run.lua @@ -0,0 +1,117 @@ +local framework = require("framework") +local mobile_id_suite = require("resources/mobile_id_spec") +local smart_id_suite = require("resources/smart_id_spec") +local me_suite = require("resources/me_spec") + +if arg[1] == "--debug" then + framework.debug(true) + print("Debug mode enabled") +end + +local results = { + start_time = os.time(), + tests = {}, + passed = 0, + failed = 0, + suite_test_count = 0, + suite_tests_passed = 0, + suite_tests_failed = 0 +} + +local function format_duration(seconds) + if seconds < 60 then + return string.format("%.2f seconds", seconds) + else + local minutes = math.floor(seconds / 60) + local remaining_seconds = seconds % 60 + return string.format("%d minutes and %.2f seconds", minutes, remaining_seconds) + end +end + +local function count_tests_in_suite(suite) + local count = 0 + for name, func in pairs(suite) do + if type(func) == "function" and name:match("^test_") then + count = count + 1 + end + end + return count +end + +local function run_test_suite(name, suite) + print("\n" .. string.rep("=", 80)) + print("Running " .. name .. " tests...") + print(string.rep("=", 80)) + + local suite_test_count = count_tests_in_suite(suite) + results.suite_test_count = results.suite_test_count + suite_test_count + + local test_start = os.time() + local success = suite.run() + local test_end = os.time() + local duration = test_end - test_start + + table.insert(results.tests, { + name = name, + success = success, + result = success and "PASSED" or "FAILED", + duration = duration, + test_count = suite_test_count + }) + + if success then + results.passed = results.passed + 1 + results.suite_tests_passed = results.suite_tests_passed + suite_test_count + print("\n✅ Test suite passed in " .. format_duration(duration) .. " (" .. suite_test_count .. " tests)") + else + results.failed = results.failed + 1 + results.suite_tests_failed = results.suite_tests_failed + suite_test_count + print("\n❌ Test suite failed after " .. format_duration(duration) .. " (" .. suite_test_count .. " tests)") + end +end + +local function check_services() + print("Checking if services are up...") + + local response = framework.loki_readiness() + if not response or response.status ~= 200 then + print("❌ Loki service is not ready") + return false + end + + local response = framework.loki_backoffice_readiness() + if not response or response.status ~= 200 then + print("❌ Loki-backoffice service is not ready") + return false + end + + print("✅ All services are ready") + return true +end + +if check_services() then + run_test_suite("Mobile-ID", mobile_id_suite) + run_test_suite("Smart-ID", smart_id_suite) + run_test_suite("Me", me_suite) +else + print("❌ Services are not ready. Skipping tests.") + os.exit(1) +end + +results.end_time = os.time() +results.total_duration = results.end_time - results.start_time + +print("\n" .. string.rep("=", 80)) +print("INTEGRATION TEST RESULTS") +print(string.rep("=", 80)) +print("Total duration: " .. format_duration(results.total_duration)) +print("Test suites: " .. results.passed .. " passed, " .. results.failed .. " failed, " .. #results.tests .. " total") +print("Tests: " .. results.suite_tests_passed .. " passed, " .. results.suite_tests_failed .. " failed, " .. results.suite_test_count .. " total") +print(string.rep("=", 80)) + +for _, test in ipairs(results.tests) do + local status_icon = test.success and "✅" or "❌" + print(status_icon .. " " .. test.name .. ": " .. test.result .. " (" .. test.test_count .. " tests in " .. format_duration(test.duration) .. ")") +end + +os.exit(results.failed > 0 and 1 or 0) diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml index d4ea384..aac69d5 100644 --- a/.github/workflows/checks.yaml +++ b/.github/workflows/checks.yaml @@ -19,6 +19,8 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.24.1' + cache: true + cache-dependency-path: go.sum - name: golangci-lint # NOTE: https://github.com/golangci/golangci-lint-action/releases/tag/v6.2.0 uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae @@ -35,6 +37,8 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.24.1' + cache: true + cache-dependency-path: go.sum - name: Run static check # NOTE: https://github.com/dominikh/staticcheck-action/releases/tag/v1.3.1 uses: dominikh/staticcheck-action@fe1dd0c3658873b46f8c9bb3291096a617310ca6 @@ -79,6 +83,8 @@ jobs: - uses: actions/setup-go@v5 with: go-version: '1.24.1' + cache: true + cache-dependency-path: go.sum - name: Load schema.sql env: diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml new file mode 100644 index 0000000..10e1eb3 --- /dev/null +++ b/.github/workflows/integration.yaml @@ -0,0 +1,170 @@ +name: Integration +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + push: + branches: + - master +concurrency: + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' + cancel-in-progress: true + +jobs: + tests: + name: Tests + permissions: + contents: read + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16.4-alpine + env: + POSTGRES_HOST: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7.4-alpine + env: + REDIS_HOST: redis + REDIS_PORT: 6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Show GitHub workspace vars + run: | + echo "GITHUB_WORKSPACE: $GITHUB_WORKSPACE" + echo "RUNNER_WORKSPACE: $RUNNER_WORKSPACE" + mkdir -p $GITHUB_WORKSPACE/tmp + + - name: Checkout loki repository + uses: actions/checkout@v4 + with: + path: loki + + - name: Checkout loki-backoffice repository + uses: actions/checkout@v4 + with: + repository: tab/loki-backoffice + path: loki-backoffice + ref: feature/grpc + token: ${{ secrets.ACCESS_TOKEN }} + + - name: Set repository paths + run: | + echo "LOKI_REPO=$GITHUB_WORKSPACE/loki" >> $GITHUB_ENV + echo "LOKI_BACKOFFICE_REPO=$GITHUB_WORKSPACE/loki-backoffice" >> $GITHUB_ENV + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.1' + cache: true + cache-dependency-path: | + ${{ env.LOKI_REPO }}/go.sum + ${{ env.LOKI_BACKOFFICE_REPO }}/go.sum + + - name: Cache Goose + id: cache-goose + uses: actions/cache@v4 + with: + path: ~/go/bin/goose + key: ${{ runner.os }}-goose-latest + + - name: Install Goose + if: steps.cache-goose.outputs.cache-hit != 'true' + run: go install github.com/pressly/goose/v3/cmd/goose@latest + + - name: Setup database + working-directory: ${{ env.LOKI_REPO }}/.github/actions/integration + env: + LOKI_REPO: ${{ env.LOKI_REPO }} + LOKI_BACKOFFICE_REPO: ${{ env.LOKI_BACKOFFICE_REPO }} + run: | + make db:create + make db:migrate + + - name: Generate certificates + working-directory: ${{ env.LOKI_REPO }}/.github/actions/integration + env: + LOKI_REPO: ${{ env.LOKI_REPO }} + LOKI_BACKOFFICE_REPO: ${{ env.LOKI_BACKOFFICE_REPO }} + run: | + make certs:generate + + - name: Cache Docker build context + uses: actions/cache@v4 + with: + path: | + ${{ env.LOKI_REPO }}/.docker-cache + ${{ env.LOKI_BACKOFFICE_REPO }}/.docker-cache + key: ${{ runner.os }}-docker-latest + + - name: Start services + working-directory: ${{ env.LOKI_REPO }}/.github/actions/integration + env: + LOKI_REPO: ${{ env.LOKI_REPO }} + LOKI_BACKOFFICE_REPO: ${{ env.LOKI_BACKOFFICE_REPO }} + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + run: | + make docker:network + make docker:start + make check:services + + - name: Cache Lua and dependencies + id: cache-lua + uses: actions/cache@v4 + with: + path: | + /usr/bin/lua* + /usr/local/lib/luarocks + /usr/local/share/lua + key: ${{ runner.os }}-lua-latest + + - name: Install Lua and dependencies + if: steps.cache-lua.outputs.cache-hit != 'true' + run: | + sudo apt-get update + sudo apt-get install -y lua5.3 liblua5.3-dev luarocks + sudo luarocks install luasocket + sudo luarocks install lua-cjson + sudo luarocks install uuid + + - name: Run integration tests + working-directory: ${{ env.LOKI_REPO }}/.github/actions/integration + run: make run + + - name: Collect logs if tests failed + if: failure() + run: | + echo "Collecting logs from services..." + mkdir -p logs + docker logs loki > logs/loki-logs.txt 2>&1 || true + docker logs loki-backoffice > logs/loki-backoffice-logs.txt 2>&1 || true + + - name: Upload test logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs + path: logs/ + if-no-files-found: ignore + + - name: Cleanup + if: always() + working-directory: ${{ env.LOKI_REPO }}/.github/actions/integration + run: make cleanup From 614d80d68b21b42640d185c6919755123ccd0734 Mon Sep 17 00:00:00 2001 From: tab Date: Sun, 20 Apr 2025 12:36:54 +0300 Subject: [PATCH 19/20] chore(certs): Use single quotes in OpenSSL command for generating CA certificate --- .github/actions/integration/generate-certs.sh | 2 +- README.md | 2 +- docs/certificates.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/integration/generate-certs.sh b/.github/actions/integration/generate-certs.sh index 96fa485..6f2d2b0 100755 --- a/.github/actions/integration/generate-certs.sh +++ b/.github/actions/integration/generate-certs.sh @@ -20,7 +20,7 @@ cp "$LOKI_REPO/certs/jwt/public.key" "$LOKI_BACKOFFICE_REPO/certs/jwt/public.key echo "Generating Certificate Authority (CA)..." openssl genrsa -out "$LOKI_REPO/certs/ca.key" 4096 -openssl req -new -x509 -key "$LOKI_REPO/certs/ca.key" -sha256 -subj "/CN=Loki CA" \ +openssl req -new -x509 -key "$LOKI_REPO/certs/ca.key" -sha256 -subj '/CN=Loki CA' \ -out "$LOKI_REPO/certs/ca.pem" -days 3650 cp "$LOKI_REPO/certs/ca.pem" "$LOKI_BACKOFFICE_REPO/certs/ca.pem" diff --git a/README.md b/README.md index e68b483..95fe425 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ openssl rsa -in certs/jwt/private.key -pubout -out certs/jwt/public.key ```sh # Generate CA openssl genrsa -out certs/ca.key 4096 -openssl req -new -x509 -key certs/ca.key -sha256 -subj "/CN=Loki CA" -out certs/ca.pem -days 3650 +openssl req -new -x509 -key certs/ca.key -sha256 -subj '/CN=Loki CA' -out certs/ca.pem -days 3650 # Generate Server Certificate openssl genrsa -out certs/server.key 4096 diff --git a/docs/certificates.md b/docs/certificates.md index 3ca582b..0840b2e 100644 --- a/docs/certificates.md +++ b/docs/certificates.md @@ -31,7 +31,7 @@ Generate a private key for your CA ```sh openssl genrsa -out certs/ca.key 4096 -openssl req -new -x509 -key certs/ca.key -sha256 -subj "/CN=Loki CA" -out certs/ca.pem -days 3650 +openssl req -new -x509 -key certs/ca.key -sha256 -subj '/CN=Loki CA' -out certs/ca.pem -days 3650 ``` ### Generate the Server Certificate From b11f31f45015cd6fa35298009155cc612e3075a4 Mon Sep 17 00:00:00 2001 From: tab Date: Sun, 20 Apr 2025 12:37:53 +0300 Subject: [PATCH 20/20] chore(Makefile): Update environment variable handling for Loki repositories --- .github/actions/integration/Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/actions/integration/Makefile b/.github/actions/integration/Makefile index b16c017..b1ede3e 100644 --- a/.github/actions/integration/Makefile +++ b/.github/actions/integration/Makefile @@ -1,5 +1,6 @@ -LOKI_REPO ?= $(shell pwd)/../../loki -LOKI_BACKOFFICE_REPO ?= $(shell pwd)/../../loki-backoffice +LOKI_HOME ?= "" +LOKI_REPO ?= ${LOKI_HOME}/loki +LOKI_BACKOFFICE_REPO ?= ${LOKI_HOME}/loki-backoffice LOKI_DB_NAME = loki-test BACKOFFICE_DB_NAME = loki-backoffice-test