diff --git a/README.md b/README.md index c85718ee13066ca8a689c63e7e3ce529559477e3..96c81e1bfc4fb8ef849e90497aece7f47d3673b1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ # Fachstellen-Proxy +Der Fachstellen-Proxy nimmt HTTP Requests von der Fachstelle entgegen, mappt diese auf gRPC +und leitet sie an den Zufi- (Fachstellenregistrierung) bzw. Collaboration-Manager +(Fachstellenbeteiligung) weiter. Zum Protokollmapping von HTTP auf gRPC wird die Bibliothek +[gRPC-Gateway](https://grpc-ecosystem.github.io/grpc-gateway/) verwendet. Dadurch können +die HTTP-Endpunkte inkl. des Mappings größtenteils automatisch aus proto-Dateien generiert werden. + +Requests zur Fachstellenregistrierung werden an die unter config.grpc.registration.server.url +eingetragene URL (bzw. localhost:50052, wenn config.grpc.mock = true) weitergeleitet. + +Requests zur Fachstellenbeteiligung (z.B. FindVorgang) werden zunächst an den CollaborationRouter +weitergeleitet. Das ist ein Proxy-eigener gRPC-Server, der auf localhost und dem unter +config.grpc.collaboration.router.port eingetragenen Port läuft. Dort wird die Adresse des +Ziel-Collaboration-Managers aus dem Requestpayload (z.B. aus der VorgangId) extrahiert. Der Port +des Ziel-Collaboration-Managers ist konstant und wird unter config.grpc.collaboration.server.port +festgelegt (bzw. auf 50052 gesetzt, wenn config.grpc.mock = true). Anschließend leitet der +CollaborationRouter die gRPC Request an die URL weiter, die aus der extrahierten Adresse und dem +Port zusammengesetzt wird. + ## Getting Started ### Dependencies installieren @@ -37,11 +55,19 @@ go run cmd/fachstellen-proxy/main.go [Config-Datei](./config/config.yml) ``` -server: - port: Port des HTTP Servers (int) +http: + server: + port: Port des HTTP Gateways (int) grpc: - mock: lokalen gRPC Server mocken (bool) - url: URL des Ziel-gRPC-Servers im host:port Format (string) + mock: gRPC Registration und Collaboration Server mocken (bool) + collaboration: + server: + port: Port des gRPC Collaboration Servers (int) + router: + port: Port des gRPC Collaboration Routers (int) + registration: + server: + url: URL des gRPC Registration Servers im host:port Format (string) logging: level: "ERROR" | "WARN" | "INFO" | "DEBUG" ``` \ No newline at end of file diff --git a/api/gateway-config.yml b/api/gateway-config.yml index b158d6f06fff7338ef972c3eb233f64f9ee2f2f6..4b88f29c962d65f5b2ab3cf329aa1ba8951a5127 100644 --- a/api/gateway-config.yml +++ b/api/gateway-config.yml @@ -3,6 +3,8 @@ config_version: 3 http: rules: - - selector: de.ozgcloud.zufi.grpc.fachstelle.FachstelleRegistrationService.Register + - selector: de.ozgcloud.fachstellenproxy.FachstelleRegistrationService.Register post: /api/fachstellen body: "*" + - selector: de.ozgcloud.fachstellenproxy.CollaborationService.FindVorgang + get: /api/vorgang/{vorgangId}/{samlToken} diff --git a/api/openapi-config.yml b/api/openapi-config.yml new file mode 100644 index 0000000000000000000000000000000000000000..f32a4dd6e5c5a8d5629b450e15a881fc7048c522 --- /dev/null +++ b/api/openapi-config.yml @@ -0,0 +1,89 @@ +openapiOptions: + file: + - file: "fachstelleregistration.proto" + option: + info: + title: Fachstelleregistration Proxy API + license: + name: EUPL 1.2 + url: https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + version: "1.0" + schemes: + - HTTP + - HTTPS + consumes: + - application/json + produces: + - application/json + responses: + "400": + description: Returned when the request payload is not suitable. + schema: + jsonSchema: + type: + - STRING + - file: "collaboration.proto" + option: + info: + title: Collaboration Proxy API + license: + name: EUPL 1.2 + url: https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + version: "1.0" + schemes: + - HTTP + - HTTPS + consumes: + - application/json + produces: + - application/json + responses: + "403": + description: Returned when the user does not have permission to access the resource. + "404": + description: Returned when the resource does not exist. + schema: + jsonSchema: + type: + - STRING + service: + - service: de.ozgcloud.fachstellenproxy.FachstelleRegistrationService + option: + description: "Service to proxy between Fachstelle and ZufiManager in OZG-Cloud" + - service: de.ozgcloud.fachstellenproxy.CollaborationService + option: + description: "Service to proxy between Fachstelle and CollaborationManager in OZG-Cloud" + method: + - method: de.ozgcloud.fachstellenproxy.FachstelleRegistrationService.Register + option: + description: "Register new fachstelle" + summary: "Summary: Register rpc" + responses: + "200": + description: Returns empty response + "400": + description: Returned when the request payload is not suitable. + "503": + description: Returned when the resource is temporarily unavailable. + - method: de.ozgcloud.fachstellenproxy.CollaborationService.FindVorgang + option: + description: "Find vorgang by its id" + summary: "Summary: FindVorgang rpc" + responses: + "200": + description: Returns the vorgang + "503": + description: Returned when the resource is temporarily unavailable. + "404": + description: Returned when the resource does not exist. + - method: de.ozgcloud.fachstellenproxy.CollaborationService.GetFileContent + option: + description: "Get file content by file id" + summary: "Summary: GetFileContent rpc" + responses: + "200": + description: Returns the file content + "503": + description: Returned when the resource is temporarily unavailable. + "404": + description: Returned when the resource does not exist. \ No newline at end of file diff --git a/api/proto/collaboration.model.proto b/api/proto/collaboration.model.proto new file mode 100644 index 0000000000000000000000000000000000000000..b51dbbdf794130e400f0cea360c13822cb33e677 --- /dev/null +++ b/api/proto/collaboration.model.proto @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +syntax = "proto3"; + +package de.ozgcloud.fachstellenproxy; + +option go_package = "de.ozgcloud.fachstellenproxy"; + +message GrpcFindVorgangRequest { + string vorgangId = 1; + string samlToken = 2; +} + +message GrpcFindVorgangResponse { + GrpcVorgang vorgang = 1; +} + +message GrpcVorgang { + string id = 1; + int64 version = 2; + string vorgangName = 3; + string vorgangNummer = 4; + + GrpcVorgangHeader header = 5; + GrpcEingang eingang = 6; + GrpcCollaborationRequest collaborationRequest = 7; +} + +message GrpcVorgangHeader { + string createdAt = 1; + string aktenzeichen = 2; +} + +message GrpcCollaborationRequest { + string title = 1; + string text = 2; +} + +message GrpcEingang { + GrpcAntragsteller antragsteller = 1; + + GrpcFormData formData = 2; + + repeated GrpcFileGroup attachments = 3; + repeated GrpcFile representations = 4; +} + +message GrpcFileGroup { + string name = 1; + repeated GrpcFile files = 2; +} + +message GrpcFile { + string id = 1; + string vendorId = 2; + string name = 3; + string contentType = 4; + int64 size = 5; +} + +message GrpcAntragsteller { + string firmaName = 1; + string anrede = 2; + string nachname = 3; + string vorname = 4; + string geburtsdatum = 5; + string geburtsort = 7; + string geburtsname = 8; + string email = 9; + string telefon = 10; + string strasse = 11; + string hausnummer = 12; + string plz = 13; + string ort = 14; + + GrpcFormData otherData = 15; +} + +message GrpcFormData { + repeated GrpcSubFormData formData = 1; +} + +message GrpcSubForm { + string name = 1; + string label = 2; + repeated GrpcSubFormData formData = 3; +} + +message GrpcSubFormData { + oneof data { + GrpcFormField field = 1; + GrpcSubForm form = 2; + } +} + +message GrpcFormField { + string name = 1; + string label = 2; + string value = 3; +} + +message GrpcGetFileContentRequest { + string samlToken = 1; + string id = 2; +} + +message GrpcGetFileContentResponse { + bytes fileContent = 1; +} \ No newline at end of file diff --git a/api/proto/collaboration.proto b/api/proto/collaboration.proto new file mode 100644 index 0000000000000000000000000000000000000000..fac2e32c3e532a0340a5d0dcf2a0000ef512f7ee --- /dev/null +++ b/api/proto/collaboration.proto @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +syntax = "proto3"; + +package de.ozgcloud.fachstellenproxy; + +import "collaboration.model.proto"; + +option go_package = "de.ozgcloud.fachstellenproxy"; + +service CollaborationService { + rpc FindVorgang(GrpcFindVorgangRequest) returns (GrpcFindVorgangResponse); + + rpc GetFileContent(GrpcGetFileContentRequest) returns (stream GrpcGetFileContentResponse); +} \ No newline at end of file diff --git a/api/proto/fachstelleregistration.model.proto b/api/proto/fachstelleregistration.model.proto index 9b9a1e18d742a8a38e340408f25a70a19b08e437..7ade3294fdd6e7bdf164fc784574f0d702eba655 100644 --- a/api/proto/fachstelleregistration.model.proto +++ b/api/proto/fachstelleregistration.model.proto @@ -25,9 +25,9 @@ syntax = "proto3"; -package de.ozgcloud.zufi.grpc.fachstelle; +package de.ozgcloud.fachstellenproxy; -option go_package = "de.ozgcloud.zufi.grpc.fachstelle"; +option go_package = "de.ozgcloud.fachstellenproxy"; message GrpcFachstelleRegistrationRequest { GrpcFachstelle fachstelle = 1; diff --git a/api/proto/fachstelleregistration.proto b/api/proto/fachstelleregistration.proto index 48fd82241469cb8aad746543cb0d91aecacd43ad..f354c0f47b3ffd5ffb324536c6aaf1b172a36f29 100644 --- a/api/proto/fachstelleregistration.proto +++ b/api/proto/fachstelleregistration.proto @@ -25,11 +25,11 @@ syntax = "proto3"; -package de.ozgcloud.zufi.grpc.fachstelle; +package de.ozgcloud.fachstellenproxy; import "fachstelleregistration.model.proto"; -option go_package = "de.ozgcloud.zufi.grpc.fachstelle"; +option go_package = "de.ozgcloud.fachstellenproxy"; service FachstelleRegistrationService { rpc Register(GrpcFachstelleRegistrationRequest) returns (GrpcFachstelleRegistrationResponse); diff --git a/buf.gen.yaml b/buf.gen.yaml index 611034ed3c665110980e0ab16e84936152522a7a..21d5c631f37d8df3cddb48450fd1a576b977b7cd 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -14,4 +14,9 @@ plugins: - paths=source_relative - grpc_api_configuration=api/gateway-config.yml - plugin: openapiv2 - out: gen/openapiv2 \ No newline at end of file + out: gen/openapiv2 + opt: + - allow_merge=true + - merge_file_name=openapiv2.json + - grpc_api_configuration=api/gateway-config.yml + - openapi_configuration=api/openapi-config.yml \ No newline at end of file diff --git a/cmd/fachstellen-proxy/main.go b/cmd/fachstellen-proxy/main.go index c6a993e9a18aedf3d364e250ff3e18c62cf8026c..2cf1bef5238d5f60eaf916ba640a3ea3d714931a 100644 --- a/cmd/fachstellen-proxy/main.go +++ b/cmd/fachstellen-proxy/main.go @@ -40,5 +40,6 @@ func main() { go mock.StartGrpcServer() } - server.StartHttpGateway(conf) + go server.StartCollaborationRouter() + server.StartHttpGateway() } diff --git a/config/config.yml b/config/config.yml index 8605b248d11050d5a8ad3c71b3e41e20eb8993d6..8a80d9ff53c5aecb6d8d2ef2aa61265ac0e0dbee 100755 --- a/config/config.yml +++ b/config/config.yml @@ -1,6 +1,10 @@ -server: - port: 8082 +http: + server: + port: 8082 grpc: mock: true + collaboration: + router: + port: 50051 logging: level: "INFO" \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index b246c3d63d3ad026e142c890f9532722f122358e..b46e14a135880eb52874f7a20cc7dc4075873e3b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -41,12 +41,26 @@ const ( ) type Config struct { - Server struct { - Port int `yaml:"port" envconfig:"SERVER_PORT"` - } `yaml:"server"` + Http struct { + Server struct { + Port int `yaml:"port" envconfig:"HTTP_SERVER_PORT"` + } `yaml:"server"` + } `yaml:"http"` Grpc struct { - Mock bool `yaml:"mock" envconfig:"GRPC_MOCK"` - Url string `yaml:"url" envconfig:"GRPC_URL"` + Mock bool `yaml:"mock" envconfig:"GRPC_MOCK"` + Collaboration struct { + Server struct { + Port int `yaml:"port" envconfig:"GRPC_COLLABORATION_SERVER_PORT"` + } `yaml:"server"` + Router struct { + Port int `yaml:"port" envconfig:"GRPC_COLLABORATION_ROUTER_PORT"` + } `yaml:"router"` + } `yaml:"collaboration"` + Registration struct { + Server struct { + Url string `yaml:"url" envconfig:"GRPC_REGISTRATION_SERVER_URL"` + } `yaml:"server"` + } `yaml:"registration"` } `yaml:"grpc"` Logging struct { Level string `yaml:"level" envconfig:"LOGGING_LEVEL"` @@ -78,8 +92,8 @@ func LoadConfig(configFilePath ...string) Config { log.Fatalf("FATAL: error reading env: %v", err) } - if len(config.Grpc.Url) > 0 && !ValidateGrpcUrl(config.Grpc.Url) { - log.Fatalf("FATAL: gRPC URL is not in host:port format") + if len(config.Grpc.Registration.Server.Url) > 0 && !ValidateGrpcUrl(config.Grpc.Registration.Server.Url) { + log.Fatalf("FATAL: gRPC Registration Server URL is not in host:port format") } return config diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 38cd14245895c0ba67c51af3f41cd7d3faf899f9..9f615b6bc639bf66da9fb3d5070123c63b70fc83 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -37,8 +37,10 @@ func TestLoadConfig(t *testing.T) { config := LoadConfig() expectedConfig := Config{} - expectedConfig.Server.Port = 8080 - expectedConfig.Grpc.Url = "localhost:50051" + expectedConfig.Http.Server.Port = 8080 + expectedConfig.Grpc.Collaboration.Server.Port = 50052 + expectedConfig.Grpc.Collaboration.Router.Port = 50051 + expectedConfig.Grpc.Registration.Server.Url = "localhost:50052" expectedConfig.Grpc.Mock = false expectedConfig.Logging.Level = "DEBUG" @@ -47,9 +49,11 @@ func TestLoadConfig(t *testing.T) { t.Run("should load config from env", func(t *testing.T) { envVars := map[string]string{ - "SERVER_PORT": "9090", - "GRPC_URL": "localhost:99999", - "LOGGING_LEVEL": "ERROR", + "HTTP_SERVER_PORT": "9090", + "GRPC_COLLABORATION_SERVER_PORT": "50052", + "GRPC_COLLABORATION_ROUTER_PORT": "50051", + "GRPC_REGISTRATION_SERVER_URL": "localhost:99999", + "LOGGING_LEVEL": "ERROR", } for key, value := range envVars { @@ -63,8 +67,10 @@ func TestLoadConfig(t *testing.T) { } expectedConfig := Config{} - expectedConfig.Server.Port = 9090 - expectedConfig.Grpc.Url = "localhost:99999" + expectedConfig.Http.Server.Port = 9090 + expectedConfig.Grpc.Collaboration.Server.Port = 50052 + expectedConfig.Grpc.Collaboration.Router.Port = 50051 + expectedConfig.Grpc.Registration.Server.Url = "localhost:99999" expectedConfig.Grpc.Mock = false expectedConfig.Logging.Level = "ERROR" @@ -72,15 +78,26 @@ func TestLoadConfig(t *testing.T) { }) t.Run("should overwrite config with env", func(t *testing.T) { - assert.NoError(t, os.Setenv("SERVER_PORT", "9090"), "Setenv SERVER_PORT should not return an error") + envVars := map[string]string{ + "HTTP_SERVER_PORT": "9090", + "GRPC_REGISTRATION_SERVER_URL": "localhost:99999", + } + + for key, value := range envVars { + assert.NoError(t, os.Setenv(key, value), "Setenv "+key+" should not return an error") + } config := LoadConfig() - assert.NoError(t, os.Unsetenv("SERVER_PORT"), "Unsetenv SERVER_PORT should not return an error") + for key := range envVars { + assert.NoError(t, os.Unsetenv(key), "Unsetenv "+key+" should not return an error") + } expectedConfig := Config{} - expectedConfig.Server.Port = 9090 - expectedConfig.Grpc.Url = "localhost:50051" + expectedConfig.Http.Server.Port = 9090 + expectedConfig.Grpc.Collaboration.Server.Port = 50052 + expectedConfig.Grpc.Collaboration.Router.Port = 50051 + expectedConfig.Grpc.Registration.Server.Url = "localhost:99999" expectedConfig.Grpc.Mock = false expectedConfig.Logging.Level = "DEBUG" diff --git a/internal/config/testdata/test_config.yml b/internal/config/testdata/test_config.yml index ea8925db9cb5af5c351a1af92d6fb7f878de4359..7cbb36dddca0cc2765c7e42c01a8c268070d8ef8 100644 --- a/internal/config/testdata/test_config.yml +++ b/internal/config/testdata/test_config.yml @@ -1,6 +1,14 @@ -server: - port: 8080 +http: + server: + port: 8080 grpc: - url: "localhost:50051" + collaboration: + server: + port: 50052 + router: + port: 50051 + registration: + server: + url: "localhost:50052" logging: level: "DEBUG" \ No newline at end of file diff --git a/internal/logging/logger_test.go b/internal/logging/logger_test.go index 11eff2f85f749fc9bcd884ee9d57c7a9b13fe12d..f0b209ca5d6f61b82ae3e38d3ef1316c8980c1f2 100644 --- a/internal/logging/logger_test.go +++ b/internal/logging/logger_test.go @@ -28,21 +28,20 @@ package logging import ( "bytes" "github.com/stretchr/testify/assert" - "log" "testing" ) -func setUpLogger(t *testing.T, level string, msg string) *bytes.Buffer { +func logMessage(level string, msg string) *bytes.Buffer { logger := GetLogger() var buf bytes.Buffer logger.BaseLogger.SetOutput(&buf) - originalFlags := log.Flags() - t.Cleanup(func() { - log.SetOutput(nil) - log.SetFlags(originalFlags) - }) + originalFlags := logger.BaseLogger.Flags() + defer func() { + logger.BaseLogger.SetOutput(nil) + logger.BaseLogger.SetFlags(originalFlags) + }() if level == LogLevelError { logger.Error(msg) @@ -58,25 +57,25 @@ func setUpLogger(t *testing.T, level string, msg string) *bytes.Buffer { } func TestError(t *testing.T) { - buf := setUpLogger(t, LogLevelError, "test error") - logOutput := buf.String() - assert.Contains(t, logOutput, "ERROR: test error") + buf := logMessage(LogLevelError, "test error") + + assert.Contains(t, buf.String(), "ERROR: test error") } func TestWarning(t *testing.T) { - buf := setUpLogger(t, LogLevelWarning, "test warning") - logOutput := buf.String() - assert.Contains(t, logOutput, "WARNING: test warning") + buf := logMessage(LogLevelWarning, "test warning") + + assert.Contains(t, buf.String(), "WARNING: test warning") } func TestInfo(t *testing.T) { - buf := setUpLogger(t, LogLevelInfo, "test info") - logOutput := buf.String() - assert.Contains(t, logOutput, "INFO: test info") + buf := logMessage(LogLevelInfo, "test info") + + assert.Contains(t, buf.String(), "INFO: test info") } func TestDebug(t *testing.T) { - buf := setUpLogger(t, LogLevelDebug, "test debug") - logOutput := buf.String() - assert.Empty(t, logOutput) + buf := logMessage(LogLevelDebug, "test debug") + + assert.Empty(t, buf.String()) } diff --git a/internal/logging/testdata/test_config.yml b/internal/logging/testdata/test_config.yml index 6021893b2e1982f4f8d4215707fa6300e2cd6e8e..64e799f15a52e1afec93177b366e66a3c8c87c88 100644 --- a/internal/logging/testdata/test_config.yml +++ b/internal/logging/testdata/test_config.yml @@ -1,6 +1,14 @@ -server: - port: 8080 +http: + server: + port: 8080 grpc: - url: "localhost:50051" + collaboration: + server: + port: 50052 + router: + port: 50051 + registration: + server: + url: "localhost:50052" logging: level: "INFO" \ No newline at end of file diff --git a/internal/mock/grpc_server.go b/internal/mock/grpc_server.go index 729525c994e777903b5877ef109e254031293bcc..bb37e171ab62ffcda7668b16baf5c484d334a8e8 100644 --- a/internal/mock/grpc_server.go +++ b/internal/mock/grpc_server.go @@ -32,41 +32,104 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "io" "net" + "os" + "testing" ) -const GrpcMockPort = 50051 +const ( + GrpcMockPort = 50052 + defaultFilePath = "internal/mock/testdata/dummy.pdf" + testFilePath = "testdata/dummy.pdf" +) -type server struct { +type registrationServer struct { pb.UnimplementedFachstelleRegistrationServiceServer } -func (s *server) Register(ctx context.Context, in *pb.GrpcFachstelleRegistrationRequest) (*pb.GrpcFachstelleRegistrationResponse, error) { +func (s *registrationServer) Register(_ context.Context, in *pb.GrpcFachstelleRegistrationRequest) (*pb.GrpcFachstelleRegistrationResponse, error) { if in.Fachstelle == nil { - return nil, status.Error(codes.InvalidArgument, "Fachstelle is missing") + return nil, status.Error(codes.InvalidArgument, "fachstelle is missing") } if in.Fachstelle.MukId == "conflictMukId" { - return nil, status.Error(codes.AlreadyExists, "Conflict mukId") + return nil, status.Error(codes.AlreadyExists, "conflict MukId") } else if in.Fachstelle.MukId == "unavailableMukId" { - return nil, status.Error(codes.Unavailable, "Unavailable mukId") + return nil, status.Error(codes.Unavailable, "unavailable MukId") } else if in.Fachstelle.MukId == "erroneousMukId" { - return nil, status.Error(codes.Internal, "Erroneous mukId") + return nil, status.Error(codes.Internal, "erroneous MukId") } return &pb.GrpcFachstelleRegistrationResponse{}, nil } +type collaborationServer struct { + pb.UnimplementedCollaborationServiceServer +} + +func (s *collaborationServer) FindVorgang(_ context.Context, in *pb.GrpcFindVorgangRequest) (*pb.GrpcFindVorgangResponse, error) { + if in.VorgangId == "" { + return nil, status.Error(codes.InvalidArgument, "vorgangId is missing") + } + + if in.SamlToken == "" { + return nil, status.Error(codes.InvalidArgument, "samlToken is missing") + } + + return &pb.GrpcFindVorgangResponse{Vorgang: &pb.GrpcVorgang{Id: "testVorgangId"}}, nil +} + +func (s *collaborationServer) GetFileContent(in *pb.GrpcGetFileContentRequest, stream pb.CollaborationService_GetFileContentServer) error { + if in.Id == "" { + return status.Error(codes.InvalidArgument, "fileId is missing") + } + + if in.SamlToken == "" { + return status.Error(codes.InvalidArgument, "samlToken is missing") + } + + fp := defaultFilePath + if testing.Testing() { + fp = testFilePath + } + + file, err := os.Open(fp) + if err != nil { + return err + } + defer file.Close() + + buffer := make([]byte, 1024*1024) + for { + bytesRead, err := file.Read(buffer) + if err == io.EOF { + break + } + if err != nil { + return err + } + + chunk := &pb.GrpcGetFileContentResponse{FileContent: buffer[:bytesRead]} + if err := stream.Send(chunk); err != nil { + return err + } + } + + return nil +} + func StartGrpcServer() *grpc.Server { s := grpc.NewServer() - pb.RegisterFachstelleRegistrationServiceServer(s, &server{}) + pb.RegisterFachstelleRegistrationServiceServer(s, ®istrationServer{}) + pb.RegisterCollaborationServiceServer(s, &collaborationServer{}) lis, err := net.Listen("tcp", fmt.Sprintf(":%d", GrpcMockPort)) if err != nil { logger.Fatal("gRPC server failed to listen: %v", err) } - logger.Info("gRPC server listening on port %v", GrpcMockPort) + logger.Info("gRPC server listening on port %d", GrpcMockPort) if err := s.Serve(lis); err != nil { logger.Fatal("gRPC server failed to serve: %v", err) } diff --git a/internal/mock/grpc_server_test.go b/internal/mock/grpc_server_test.go index b7a4309ea9fb7b52f372928488b849f5c3ef84ca..b9ab84c0db470f438181cb9121cbc8e0340189a4 100644 --- a/internal/mock/grpc_server_test.go +++ b/internal/mock/grpc_server_test.go @@ -32,41 +32,103 @@ import ( "github.com/stretchr/testify/assert" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "net" "testing" + "time" ) func TestStartGrpcServer(t *testing.T) { - setUpGrpcEnv := func() (pb.FachstelleRegistrationServiceClient, func()) { + t.Run("should start gRPC server", func(t *testing.T) { SetUpGrpcServer() - conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", GrpcMockPort), grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := net.DialTimeout("tcp", fmt.Sprintf("localhost:%d", GrpcMockPort), 2*time.Second) + assert.NoError(t, err) - cleanup := func() { - conn.Close() + conn.Close() + }) + + t.Run("TestFachstelleRegistrationEndpoints", func(t *testing.T) { + setUpGrpcEnv := func() (pb.FachstelleRegistrationServiceClient, func()) { + SetUpGrpcServer() + + conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", GrpcMockPort), grpc.WithTransportCredentials(insecure.NewCredentials())) + assert.NoError(t, err) + + cleanUp := func() { + conn.Close() + } + client := pb.NewFachstelleRegistrationServiceClient(conn) + + return client, cleanUp } - client := pb.NewFachstelleRegistrationServiceClient(conn) - return client, cleanup - } + t.Run("should have no error", func(t *testing.T) { + client, cleanUp := setUpGrpcEnv() + defer cleanUp() - t.Run("should have no error", func(t *testing.T) { - client, cleanUp := setUpGrpcEnv() - defer cleanUp() + resp, err := client.Register(context.Background(), &pb.GrpcFachstelleRegistrationRequest{Fachstelle: &pb.GrpcFachstelle{MukId: "testMukId"}}) - resp, err := client.Register(context.Background(), &pb.GrpcFachstelleRegistrationRequest{Fachstelle: &pb.GrpcFachstelle{MukId: "testMukId"}}) + assert.NoError(t, err) + assert.NotNil(t, resp) + }) - assert.NoError(t, err) - assert.NotNil(t, resp) + t.Run("should have error", func(t *testing.T) { + client, cleanUp := setUpGrpcEnv() + defer cleanUp() + + resp, err := client.Register(context.Background(), &pb.GrpcFachstelleRegistrationRequest{}) + + assert.Error(t, err) + assert.Nil(t, resp) + }) }) - t.Run("should have error", func(t *testing.T) { - client, cleanUp := setUpGrpcEnv() - defer cleanUp() + t.Run("TestCollaborationEndpoints", func(t *testing.T) { + setUpGrpcEnv := func() (pb.CollaborationServiceClient, func()) { + SetUpGrpcServer() + + conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", GrpcMockPort), grpc.WithTransportCredentials(insecure.NewCredentials())) + assert.NoError(t, err) + + cleanup := func() { + conn.Close() + } + client := pb.NewCollaborationServiceClient(conn) + + return client, cleanup + } + + t.Run("TestFindVorgang", func(t *testing.T) { + t.Run("should have no error", func(t *testing.T) { + client, cleanUp := setUpGrpcEnv() + defer cleanUp() + + resp, err := client.FindVorgang(context.Background(), &pb.GrpcFindVorgangRequest{VorgangId: "testVorgangId", SamlToken: "testSamlToken"}) + + assert.NoError(t, err) + assert.NotNil(t, resp) + }) + + t.Run("should have error", func(t *testing.T) { + client, cleanUp := setUpGrpcEnv() + defer cleanUp() + + resp, err := client.FindVorgang(context.Background(), &pb.GrpcFindVorgangRequest{}) + + assert.Error(t, err) + assert.Nil(t, resp) + }) + }) + + t.Run("TestGetFileContent", func(t *testing.T) { + client, cleanUp := setUpGrpcEnv() + defer cleanUp() - resp, err := client.Register(context.Background(), &pb.GrpcFachstelleRegistrationRequest{}) + resp, err := client.GetFileContent(context.Background(), &pb.GrpcGetFileContentRequest{}) - assert.Error(t, err) - assert.Nil(t, resp) + assert.NoError(t, err) + assert.NotNil(t, resp) + }) }) } diff --git a/internal/mock/testdata/dummy.pdf b/internal/mock/testdata/dummy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..02dcdbb6d5954ac6019da351a5ad93f191e846aa Binary files /dev/null and b/internal/mock/testdata/dummy.pdf differ diff --git a/internal/mock/testdata/test_config.yml b/internal/mock/testdata/test_config.yml index ea8925db9cb5af5c351a1af92d6fb7f878de4359..7cbb36dddca0cc2765c7e42c01a8c268070d8ef8 100644 --- a/internal/mock/testdata/test_config.yml +++ b/internal/mock/testdata/test_config.yml @@ -1,6 +1,14 @@ -server: - port: 8080 +http: + server: + port: 8080 grpc: - url: "localhost:50051" + collaboration: + server: + port: 50052 + router: + port: 50051 + registration: + server: + url: "localhost:50052" logging: level: "DEBUG" \ No newline at end of file diff --git a/internal/server/globals.go b/internal/server/globals.go index 2ac6c4f087be0e6c7ac987c16203e805fe33a552..e0ac55ecc9acee1da4dd01afc4f5bb237bfc3d6d 100644 --- a/internal/server/globals.go +++ b/internal/server/globals.go @@ -1,5 +1,14 @@ package server -import "fachstellen-proxy/internal/logging" +import ( + "fachstellen-proxy/internal/config" + "fachstellen-proxy/internal/logging" +) +const ( + GrpcAddressHeader = "X-Grpc-Address" + GrpcAddressMetadata = "x-grpc-address" +) + +var conf = config.LoadConfig() var logger = logging.GetLogger() diff --git a/internal/server/grpc_router.go b/internal/server/grpc_router.go new file mode 100644 index 0000000000000000000000000000000000000000..09559ce8ac4226f2e63080d6e03cc9ca3436b1ed --- /dev/null +++ b/internal/server/grpc_router.go @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023-2024 + * Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package server + +import ( + "context" + "errors" + pb "fachstellen-proxy/gen/go" + "fachstellen-proxy/internal/config" + "fachstellen-proxy/internal/mock" + "fmt" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "net" +) + +type collaborationRouter struct { + pb.UnimplementedCollaborationServiceServer +} + +func (s *collaborationRouter) FindVorgang(ctx context.Context, in *pb.GrpcFindVorgangRequest) (*pb.GrpcFindVorgangResponse, error) { + response, err := s.handleRequest(ctx, func(client pb.CollaborationServiceClient, ctx context.Context) (interface{}, error) { + return client.FindVorgang(ctx, in) + }) + + if response == nil { + return nil, err + } + + return response.(*pb.GrpcFindVorgangResponse), err +} + +func (s *collaborationRouter) handleRequest(ctx context.Context, handler func(pb.CollaborationServiceClient, context.Context) (interface{}, error)) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.InvalidArgument, "unable to retrieve metadata") + } + + grpcAddress := md.Get(GrpcAddressMetadata) + if len(grpcAddress) == 0 { + return nil, status.Error(codes.InvalidArgument, "grpc address is missing") + } + + client, connCleanUp, err := createCollaborationClient(grpcAddress[0]) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + defer connCleanUp() + + return handler(client, ctx) +} + +func createCollaborationClient(grpcAddress string) (pb.CollaborationServiceClient, func() error, error) { + target := GetCollaborationServerUrl(grpcAddress, conf) + conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + logger.Error("collaboration router failed to route: %v", err) + + return nil, nil, errors.New("collaboration router failed to route") + } + + return pb.NewCollaborationServiceClient(conn), conn.Close, nil +} + +func GetCollaborationServerUrl(collaborationServerAddress string, c config.Config) string { + collaborationServerPort := c.Grpc.Collaboration.Server.Port + if c.Grpc.Mock { + collaborationServerPort = mock.GrpcMockPort + } + + return fmt.Sprintf("%v:%d", collaborationServerAddress, collaborationServerPort) +} + +func StartCollaborationRouter() *grpc.Server { + s := grpc.NewServer() + pb.RegisterCollaborationServiceServer(s, &collaborationRouter{}) + + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", conf.Grpc.Collaboration.Router.Port)) + if err != nil { + logger.Fatal("collaboration router failed to listen: %v", err) + } + + logger.Info("collaboration router listening on port %d", conf.Grpc.Collaboration.Router.Port) + if err := s.Serve(lis); err != nil { + logger.Fatal("collaboration router failed to serve: %v", err) + } + + return s +} diff --git a/internal/server/grpc_router_test.go b/internal/server/grpc_router_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3f5d765d85dd9300fcd1edda59f4a542448ea889 --- /dev/null +++ b/internal/server/grpc_router_test.go @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023-2024 + * Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package server + +import ( + "context" + pb "fachstellen-proxy/gen/go" + "fachstellen-proxy/internal/config" + "fachstellen-proxy/internal/mock" + "fmt" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "net" + "testing" + "time" +) + +func TestStartCollaborationRouter(t *testing.T) { + setUpGrpcRouterEnv := func() (pb.CollaborationServiceClient, func()) { + mock.SetUpGrpcServer() + SetUpCollaborationRouter() + + conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials())) + assert.NoError(t, err) + + cleanUp := func() { + conn.Close() + } + client := pb.NewCollaborationServiceClient(conn) + + return client, cleanUp + } + + t.Run("should start collaboration router", func(t *testing.T) { + SetUpCollaborationRouter() + + conn, err := net.DialTimeout("tcp", "localhost:50051", 2*time.Second) + + assert.NoError(t, err) + + conn.Close() + }) + + t.Run("should have no error", func(t *testing.T) { + client, cleanUp := setUpGrpcRouterEnv() + defer cleanUp() + + md := metadata.New(map[string]string{GrpcAddressMetadata: "localhost"}) + ctx := metadata.NewOutgoingContext(context.Background(), md) + resp, err := client.FindVorgang(ctx, &pb.GrpcFindVorgangRequest{VorgangId: "testVorgangId", SamlToken: "testSamlToken"}) + + assert.NoError(t, err) + assert.NotNil(t, resp) + }) + + t.Run("should have error", func(t *testing.T) { + client, cleanUp := setUpGrpcRouterEnv() + defer cleanUp() + + md := metadata.New(map[string]string{GrpcAddressMetadata: "localhost"}) + ctx := metadata.NewOutgoingContext(context.Background(), md) + resp, err := client.FindVorgang(ctx, &pb.GrpcFindVorgangRequest{}) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "vorgangId is missing") + assert.Nil(t, resp) + }) +} + +func TestGetCollaborationServerUrl(t *testing.T) { + t.Run("should have configured server url", func(t *testing.T) { + c := config.Config{} + c.Grpc.Collaboration.Server.Port = 99999 + + result := GetCollaborationServerUrl("localhost", c) + + assert.Equal(t, "localhost:99999", result) + }) + + t.Run("should have mock server url", func(t *testing.T) { + c := config.Config{} + c.Grpc.Mock = true + + result := GetCollaborationServerUrl("localhost", c) + + assert.Equal(t, fmt.Sprintf("localhost:%d", mock.GrpcMockPort), result) + }) +} diff --git a/internal/server/handler.go b/internal/server/handler.go index 3d22eb8fc72bcd32d2e28f676fb07f87aeafdd13..7b024d5b2cf0f44b14e36a1c899c4ffd7aa0cfeb 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -26,14 +26,25 @@ package server import ( + "bytes" "context" + pb "fachstellen-proxy/gen/go" "fmt" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/status" + "io" "net/http" ) +const ( + fileIdParam = "id" + samlTokenParam = "samlToken" + contentTypeHeaderKey = "Content-Type" +) + func RegisterHomeEndpoint(mux *runtime.ServeMux) { err := mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { defer func() { @@ -49,6 +60,81 @@ func RegisterHomeEndpoint(mux *runtime.ServeMux) { } } +func RegisterFachstelleRegistrationEndpoints(ctx context.Context, mux *runtime.ServeMux, grpcUrl string) { + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + err := pb.RegisterFachstelleRegistrationServiceHandlerFromEndpoint(ctx, mux, grpcUrl, opts) + if err != nil { + logger.Fatal("failed to register fachstelle registration endpoints: %v", err) + } +} + +func RegisterCollaborationEndpoints(ctx context.Context, mux *runtime.ServeMux, grpcUrl string) { + opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} + err := pb.RegisterCollaborationServiceHandlerFromEndpoint(ctx, mux, grpcUrl, opts) + if err != nil { + logger.Fatal("failed to register collaboration endpoints: %v", err) + } +} + +func RegisterGetFileEndpoint(mux *runtime.ServeMux) { + err := mux.HandlePath("GET", "/api/file/{"+fileIdParam+"}/{"+samlTokenParam+"}", getFileHandler) + + if err != nil { + logger.Fatal("failed to register get file endpoint: %v", err) + } +} + +func getFileHandler(w http.ResponseWriter, r *http.Request, vars map[string]string) { + fileBuffer, err := fetchFileFromGrpc(r.Context(), vars[fileIdParam], vars[samlTokenParam], r.Header.Get(GrpcAddressHeader)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set(contentTypeHeaderKey, "application/octet-stream") + w.WriteHeader(http.StatusOK) + + _, err = fileBuffer.WriteTo(w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func fetchFileFromGrpc(ctx context.Context, fileId string, samlToken string, grpcAddress string) (*bytes.Buffer, error) { + var fileBuffer bytes.Buffer + + target := GetCollaborationServerUrl(grpcAddress, conf) + conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("gRPC connection error") + } + defer conn.Close() + + client := pb.NewCollaborationServiceClient(conn) + req := &pb.GrpcGetFileContentRequest{Id: fileId, SamlToken: samlToken} + clientStream, err := client.GetFileContent(ctx, req) + if err != nil { + return nil, fmt.Errorf("stream creation error") + } + + for { + resp, err := clientStream.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("error receiving file chunks") + } + + _, err = fileBuffer.Write(resp.FileContent) + if err != nil { + return nil, fmt.Errorf("error writing file data") + } + } + + return &fileBuffer, nil +} + func ErrorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) { st, ok := status.FromError(err) if !ok { diff --git a/internal/server/handler_test.go b/internal/server/handler_test.go index 6cf686e466a16158d655f42a73d99d84f8118564..976134a1502454e94dfcac15caef383b24186d65 100644 --- a/internal/server/handler_test.go +++ b/internal/server/handler_test.go @@ -28,7 +28,6 @@ package server import ( "bytes" "fachstellen-proxy/internal/mock" - "fmt" "github.com/stretchr/testify/assert" "net/http" "testing" @@ -43,12 +42,54 @@ func TestRegisterHomeEndpoint(t *testing.T) { assert.Equal(t, http.StatusOK, resp.StatusCode) } +func TestRegisterFachstelleRegistrationEndpoints(t *testing.T) { + mock.SetUpGrpcServer() + SetUpHttpGateway() + + jsonData := []byte(`{"fachstelle": {"mukId": "testMukId"}}`) + resp, err := http.Post("http://localhost:8080/api/fachstellen", "application/json", bytes.NewBuffer(jsonData)) + + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRegisterCollaborationEndpoints(t *testing.T) { + mock.SetUpGrpcServer() + SetUpCollaborationRouter() + SetUpHttpGateway() + + jsonData := []byte(``) + req, err := http.NewRequest("GET", "http://localhost:8080/api/vorgang/testVorgangId/testSamlToken", bytes.NewBuffer(jsonData)) + req.Header.Set(GrpcAddressHeader, "localhost") + + client := &http.Client{} + resp, err := client.Do(req) + + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + +func TestRegisterGetFileEndpoint(t *testing.T) { + mock.SetUpGrpcServer() + SetUpHttpGateway() + + jsonData := []byte(``) + req, err := http.NewRequest("GET", "http://localhost:8080/api/file/testId/testToken", bytes.NewBuffer(jsonData)) + req.Header.Set(GrpcAddressHeader, "localhost") + + client := &http.Client{} + resp, err := client.Do(req) + + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) +} + func TestErrorHandler(t *testing.T) { assertErroneousRequest := func(jsonData []byte, expectedStatusCode int) { mock.SetUpGrpcServer() SetUpHttpGateway() - resp, err := http.Post(fmt.Sprintf("http://localhost:8080%v", RegisterFachstelleUrlPath), "application/json", bytes.NewBuffer(jsonData)) + resp, err := http.Post("http://localhost:8080/api/fachstellen", "application/json", bytes.NewBuffer(jsonData)) assert.NoError(t, err) assert.Equal(t, expectedStatusCode, resp.StatusCode) diff --git a/internal/server/http_gateway.go b/internal/server/http_gateway.go index 5906889cb9e2847e6c6466443fc813ec4adafde7..f3ae00a49b461a9e2c9d46b6be580cb71a0a2978 100644 --- a/internal/server/http_gateway.go +++ b/internal/server/http_gateway.go @@ -30,43 +30,48 @@ import ( "fachstellen-proxy/internal/config" "fachstellen-proxy/internal/mock" "fmt" - "google.golang.org/grpc/credentials/insecure" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "net/http" +) - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "google.golang.org/grpc" +func HeaderMatcher(header string) (string, bool) { + if header == GrpcAddressHeader { + return header, true + } - pb "fachstellen-proxy/gen/go" -) + return header, false +} -func StartHttpGateway(conf config.Config) *http.Server { +func StartHttpGateway() *http.Server { ctx := context.Background() ctx, cancel := context.WithCancel(ctx) defer cancel() - grpcEndpoint := conf.Grpc.Url - if conf.Grpc.Mock { - grpcEndpoint = fmt.Sprintf("localhost:%d", mock.GrpcMockPort) - } - - mux := runtime.NewServeMux(runtime.WithErrorHandler(ErrorHandler)) - opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())} - err := pb.RegisterFachstelleRegistrationServiceHandlerFromEndpoint(ctx, mux, grpcEndpoint, opts) - if err != nil { - logger.Fatal("failed to start HTTP gateway: %v", err) - } + mux := runtime.NewServeMux(runtime.WithErrorHandler(ErrorHandler), runtime.WithIncomingHeaderMatcher(HeaderMatcher)) + collaborationRouterUrl := fmt.Sprintf("localhost:%d", conf.Grpc.Collaboration.Router.Port) RegisterHomeEndpoint(mux) + RegisterFachstelleRegistrationEndpoints(ctx, mux, GetRegistrationServerUrl(conf)) + RegisterCollaborationEndpoints(ctx, mux, collaborationRouterUrl) + RegisterGetFileEndpoint(mux) httpServer := &http.Server{ - Addr: fmt.Sprintf(":%d", conf.Server.Port), + Addr: fmt.Sprintf(":%d", conf.Http.Server.Port), Handler: RequestLoggingMiddleware(mux), } - logger.Info("HTTP gateway listening on port %d", conf.Server.Port) + logger.Info("HTTP gateway listening on port %d", conf.Http.Server.Port) if err := httpServer.ListenAndServe(); err != nil { logger.Fatal("HTTP gateway failed to serve: %v", err) } return httpServer } + +func GetRegistrationServerUrl(c config.Config) string { + if c.Grpc.Mock { + return fmt.Sprintf("localhost:%d", mock.GrpcMockPort) + } + + return c.Grpc.Registration.Server.Url +} diff --git a/internal/server/http_gateway_test.go b/internal/server/http_gateway_test.go index 29fcb76ac00bf62862b9090fb20962fdf7abdb6c..7c53380592f678593d2ac4aa7310db0d7568998f 100644 --- a/internal/server/http_gateway_test.go +++ b/internal/server/http_gateway_test.go @@ -26,21 +26,55 @@ package server import ( - "bytes" + "fachstellen-proxy/internal/config" "fachstellen-proxy/internal/mock" "fmt" "github.com/stretchr/testify/assert" - "net/http" + "net" "testing" + "time" ) +func TestHeaderMatcher(t *testing.T) { + t.Run("should accept header", func(t *testing.T) { + _, accepted := HeaderMatcher(GrpcAddressHeader) + + assert.True(t, accepted) + }) + + t.Run("should reject header", func(t *testing.T) { + _, accepted := HeaderMatcher("X-Rejected-Header") + + assert.False(t, accepted) + }) +} + func TestStartHttpGateway(t *testing.T) { - mock.SetUpGrpcServer() SetUpHttpGateway() - jsonData := []byte(`{"fachstelle": {"mukId": "testMukId"}}`) - resp, err := http.Post(fmt.Sprintf("http://localhost:8080%v", RegisterFachstelleUrlPath), "application/json", bytes.NewBuffer(jsonData)) + conn, err := net.DialTimeout("tcp", "localhost:8080", 2*time.Second) assert.NoError(t, err) - assert.Equal(t, http.StatusOK, resp.StatusCode) + + conn.Close() +} + +func TestGetRegistrationServerUrl(t *testing.T) { + t.Run("should have configured server url", func(t *testing.T) { + c := config.Config{} + c.Grpc.Registration.Server.Url = "localhost:99999" + + result := GetRegistrationServerUrl(c) + + assert.Equal(t, "localhost:99999", result) + }) + + t.Run("should have mock server url", func(t *testing.T) { + c := config.Config{} + c.Grpc.Mock = true + + result := GetRegistrationServerUrl(c) + + assert.Equal(t, fmt.Sprintf("localhost:%d", mock.GrpcMockPort), result) + }) } diff --git a/internal/server/middleware.go b/internal/server/middleware.go index fdab80b369121d04af22fe2cc6a0f2dd85a01b34..41710750de6006fed161507fd8ba65846ef1601b 100644 --- a/internal/server/middleware.go +++ b/internal/server/middleware.go @@ -32,7 +32,7 @@ import ( "net/http" ) -const RegisterFachstelleUrlPath = "/api/fachstellen" +const HomeUrlPath = "/" type logResponseWriter struct { http.ResponseWriter @@ -46,28 +46,28 @@ func (rsp *logResponseWriter) WriteHeader(statusCode int) { func RequestLoggingMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != RegisterFachstelleUrlPath { + if r.URL.Path == HomeUrlPath { h.ServeHTTP(w, r) return } body, err := io.ReadAll(r.Body) if err != nil { - logger.Error("failed to read request body: %v", err) + logger.Error("failed to read %v request body for %v: %v", r.Method, r.URL.Path, err) http.Error(w, fmt.Sprintf("failed to read request body: %v", err), http.StatusBadRequest) return } r.Body = io.NopCloser(bytes.NewReader(body)) - logger.Debug("received request with body: %v", string(body)) + logger.Debug("received %v request for %v with body: %v", r.Method, r.URL.Path, string(body)) lw := &logResponseWriter{w, http.StatusOK} h.ServeHTTP(lw, r) if lw.statusCode == http.StatusOK { - logger.Debug("successfully handled request with body: %v", string(body)) + logger.Debug("successfully handled %v request for %v with body: %v", r.Method, r.URL.Path, string(body)) } else { - logger.Error("failed handling request with body: %v, status code: %d", string(body), lw.statusCode) + logger.Error("failed handling %v request for %v with body: %v, status code: %d", r.Method, r.URL.Path, string(body), lw.statusCode) } }) } diff --git a/internal/server/middleware_test.go b/internal/server/middleware_test.go index b69bed46805ec058e48d8b01d33b0f50eb075901..90806e9ca0b4077e8383c3f1cb089f7fdfd0f70e 100644 --- a/internal/server/middleware_test.go +++ b/internal/server/middleware_test.go @@ -28,30 +28,49 @@ package server import ( "bytes" "fachstellen-proxy/internal/mock" - "fmt" "github.com/stretchr/testify/assert" - "log" "net/http" "testing" ) func TestRequestLoggingMiddleware(t *testing.T) { - mock.SetUpGrpcServer() - SetUpHttpGateway() + t.Run("should log request", func(t *testing.T) { + mock.SetUpGrpcServer() + SetUpHttpGateway() - var buf bytes.Buffer - logger.BaseLogger.SetOutput(&buf) + var buf bytes.Buffer + logger.BaseLogger.SetOutput(&buf) - originalFlags := log.Flags() - defer func() { - log.SetOutput(nil) - log.SetFlags(originalFlags) - }() + originalFlags := logger.BaseLogger.Flags() + defer func() { + logger.BaseLogger.SetOutput(nil) + logger.BaseLogger.SetFlags(originalFlags) + }() - jsonData := []byte(`{"fachstelle": {"mukId": "testMukId"}}`) - http.Post(fmt.Sprintf("http://localhost:8080%v", RegisterFachstelleUrlPath), "application/json", bytes.NewBuffer(jsonData)) + jsonData := []byte(`{"fachstelle": {"mukId": "testMukId"}}`) + http.Post("http://localhost:8080/api/fachstellen", "application/json", bytes.NewBuffer(jsonData)) - logOutput := buf.String() - assert.Contains(t, logOutput, "received request with body: {\"fachstelle\": {\"mukId\": \"testMukId\"}}") - assert.Contains(t, logOutput, "successfully handled request with body: {\"fachstelle\": {\"mukId\": \"testMukId\"}}") + logOutput := buf.String() + assert.Contains(t, logOutput, "received POST request for /api/fachstellen with body: {\"fachstelle\": {\"mukId\": \"testMukId\"}}") + assert.Contains(t, logOutput, "successfully handled POST request for /api/fachstellen with body: {\"fachstelle\": {\"mukId\": \"testMukId\"}}") + }) + + t.Run("should not log request", func(t *testing.T) { + mock.SetUpGrpcServer() + SetUpHttpGateway() + + var buf bytes.Buffer + logger.BaseLogger.SetOutput(&buf) + + originalFlags := logger.BaseLogger.Flags() + defer func() { + logger.BaseLogger.SetOutput(nil) + logger.BaseLogger.SetFlags(originalFlags) + }() + + http.Get("http://localhost:8080/") + + logOutput := buf.String() + assert.Empty(t, logOutput) + }) } diff --git a/internal/server/test_setup.go b/internal/server/test_setup.go index 5c3fd654df03385a9d008e716c93c7a60843b6cf..80bf7b2ea62875de84469231a028aa966cc0b1ab 100644 --- a/internal/server/test_setup.go +++ b/internal/server/test_setup.go @@ -26,18 +26,23 @@ package server import ( - "fachstellen-proxy/internal/config" "sync" "time" ) -var setUpOnce sync.Once +var setUpHttpGatewayOnce sync.Once +var setUpCollaborationRouterOnce sync.Once func SetUpHttpGateway() { - setUpOnce.Do(func() { - conf := config.LoadConfig() - - go StartHttpGateway(conf) + setUpHttpGatewayOnce.Do(func() { + go StartHttpGateway() time.Sleep(time.Second) // Wait for the server to start }) } + +func SetUpCollaborationRouter() { + setUpCollaborationRouterOnce.Do(func() { + go StartCollaborationRouter() + time.Sleep(time.Second) // Wait for the router to start + }) +} diff --git a/internal/server/testdata/dummy.pdf b/internal/server/testdata/dummy.pdf new file mode 100644 index 0000000000000000000000000000000000000000..02dcdbb6d5954ac6019da351a5ad93f191e846aa Binary files /dev/null and b/internal/server/testdata/dummy.pdf differ diff --git a/internal/server/testdata/test_config.yml b/internal/server/testdata/test_config.yml index 6235966c8828f67d39820aeb96b70781ca9300f7..c18dc59a3584edbc4f5b1bfdb480c716d78bf866 100644 --- a/internal/server/testdata/test_config.yml +++ b/internal/server/testdata/test_config.yml @@ -1,6 +1,10 @@ -server: - port: 8080 +http: + server: + port: 8080 grpc: + collaboration: + router: + port: 50051 mock: true logging: level: "DEBUG" \ No newline at end of file diff --git a/src/main/helm/templates/deployment.yaml b/src/main/helm/templates/deployment.yaml index ffcc3c8e6d1ec47d533e67f9abd17aab29c03fb2..cb1dc50237f22fdfcce25f2b96c09c124e28c9d5 100644 --- a/src/main/helm/templates/deployment.yaml +++ b/src/main/helm/templates/deployment.yaml @@ -58,12 +58,14 @@ spec: app.kubernetes.io/name: {{ .Release.Name }} containers: - env: - - name: SERVER_PORT + - name: HTTP_SERVER_PORT value: "8082" - name: GRPC_MOCK value: "{{ (.Values.grpc).mock | default false }}" - - name: GRPC_URL - value: "{{ required ".Values.grpc.url must be set" (.Values.grpc).url }}" + - name: GRPC_REGISTRATION_SERVER_URL + value: "{{ required ".Values.grpc.registration.server.url must be set" (((.Values.grpc).registration).server).url }}" + - name: GRPC_COLLABORATION_SERVER_PORT + value: "9090" {{- with include "app.getCustomList" . }} {{ . | indent 10 }} {{- end }} diff --git a/src/test/helm-linter-values.yaml b/src/test/helm-linter-values.yaml index bab44e4f88591b71dd0cb0220909949f70cfc0e4..2e0237dfb94f4f00c52bfa9693470b69367f4eb9 100644 --- a/src/test/helm-linter-values.yaml +++ b/src/test/helm-linter-values.yaml @@ -1,14 +1,38 @@ - +# +# Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den +# Ministerpräsidenten des Landes Schleswig-Holstein +# Staatskanzlei +# Abteilung Digitalisierung und zentrales IT-Management der Landesregierung +# +# Lizenziert unter der EUPL, Version 1.2 oder - sobald +# diese von der Europäischen Kommission genehmigt wurden - +# Folgeversionen der EUPL ("Lizenz"); +# Sie dürfen dieses Werk ausschließlich gemäß +# dieser Lizenz nutzen. +# Eine Kopie der Lizenz finden Sie hier: +# +# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +# +# Sofern nicht durch anwendbare Rechtsvorschriften +# gefordert oder in schriftlicher Form vereinbart, wird +# die unter der Lizenz verbreitete Software "so wie sie +# ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - +# ausdrücklich oder stillschweigend - verbreitet. +# Die sprachspezifischen Genehmigungen und Beschränkungen +# unter der Lizenz sind dem Lizenztext zu entnehmen. +# networkPolicy: dnsServerNamespace: test-dns-namespace - zufiManager: namespace: zufi imagePullSecret: ozgcloud-image-pull-secret -server: - port: 8080 +http: + server: + port: 8080 grpc: - url: "zufi.de:9090" \ No newline at end of file + registration: + server: + url: "zufi.de:9090" \ No newline at end of file diff --git a/src/test/helm/deployment_defaults_labels_test.yaml b/src/test/helm/deployment_defaults_labels_test.yaml index 3b2ddae990c85307b257db1c6aef07da97991bdc..18ddbba77713c96f84851e3bda4e398e8d474fd5 100644 --- a/src/test/helm/deployment_defaults_labels_test.yaml +++ b/src/test/helm/deployment_defaults_labels_test.yaml @@ -30,10 +30,18 @@ templates: - templates/deployment.yaml set: imagePullSecret: image-pull-secret - server: - port: 8080 + http: + server: + port: 8080 grpc: - url: "zufi.de:9090" + collaboration: + router: + port: 50051 + server: + port: 50052 + registration: + server: + url: "zufi.de:9090" tests: - it: check metadata.labels asserts: diff --git a/src/test/helm/deployment_env_test.yaml b/src/test/helm/deployment_env_test.yaml index 291459906207c5e4cceb50f29962d43e5e58ed6c..19ea5ae90e4e905fa3162534a29291c4663cc484 100644 --- a/src/test/helm/deployment_env_test.yaml +++ b/src/test/helm/deployment_env_test.yaml @@ -36,7 +36,9 @@ tests: - name: test_environment value: "B test value" grpc: - url: "zufi.de:9090" + registration: + server: + url: "zufi.de:9090" asserts: - contains: path: spec.template.spec.containers[0].env @@ -54,7 +56,9 @@ tests: my_test_environment_name: "A test value" test_environment: "B test value" grpc: - url: "zufi.de:9090" + registration: + server: + url: "zufi.de:9090" asserts: - contains: path: spec.template.spec.containers[0].env @@ -70,12 +74,19 @@ tests: - it: check envs set: grpc: - url: "zufi.de:9090" + registration: + server: + url: "zufi.de:9090" asserts: - contains: path: spec.template.spec.containers[0].env content: - name: GRPC_URL + name: GRPC_COLLABORATION_SERVER_PORT + value: "9090" + - contains: + path: spec.template.spec.containers[0].env + content: + name: GRPC_REGISTRATION_SERVER_URL value: "zufi.de:9090" - contains: path: spec.template.spec.containers[0].env @@ -85,10 +96,10 @@ tests: - contains: path: spec.template.spec.containers[0].env content: - name: SERVER_PORT + name: HTTP_SERVER_PORT value: "8082" - - it: should fail template if grpc.url not set + - it: should fail template if grpc.registration.server.url not set set: asserts: - failedTemplate: - errormessage: ".Values.grpc.url must be set" \ No newline at end of file + errormessage: ".Values.grpc.registration.server.url must be set" diff --git a/src/test/helm/deployment_imagepull_secret_test.yaml b/src/test/helm/deployment_imagepull_secret_test.yaml index bbfb1b1f114d95e7e87e1b6e16e0e1fc31e85090..6e3d7f932494d6185a8bb9bb49677ca9cb923b3c 100644 --- a/src/test/helm/deployment_imagepull_secret_test.yaml +++ b/src/test/helm/deployment_imagepull_secret_test.yaml @@ -29,10 +29,18 @@ release: templates: - templates/deployment.yaml set: - server: - port: 8080 + http: + server: + port: 8080 grpc: - url: "zufi.de:9090" + collaboration: + router: + port: 50051 + server: + port: 50052 + registration: + server: + url: "zufi.de:9090" tests: - it: should use correct imagePull secret set: diff --git a/src/test/helm/deployment_resources_test.yaml b/src/test/helm/deployment_resources_test.yaml index c88ff160680f16afdfb2e8e7fd3cf639303fbc5e..5af9adb1d4d1473267eab1679bfe07af0de714f9 100644 --- a/src/test/helm/deployment_resources_test.yaml +++ b/src/test/helm/deployment_resources_test.yaml @@ -29,10 +29,18 @@ templates: - templates/deployment.yaml set: imagePullSecret: image-pull-secret - server: - port: 8080 + http: + server: + port: 8080 grpc: - url: "zufi.de:9090" + collaboration: + router: + port: 50051 + server: + port: 50052 + registration: + server: + url: "zufi.de:9090" tests: - it: should generate resources when values set diff --git a/src/test/helm/deployment_test.yaml b/src/test/helm/deployment_test.yaml index b4b72cdd3745818b41046a4035795d050a9a2a7e..c04f82edfedb732187e220f54b3145bd0c971db9 100644 --- a/src/test/helm/deployment_test.yaml +++ b/src/test/helm/deployment_test.yaml @@ -29,10 +29,18 @@ templates: - templates/deployment.yaml set: imagePullSecret: image-pull-secret - server: - port: 8080 + http: + server: + port: 8080 grpc: - url: "zufi.de:9090" + collaboration: + router: + port: 50051 + server: + port: 50052 + registration: + server: + url: "zufi.de:9090" tests: - it: should have metadata values