diff --git a/handler_test.go b/handler_test.go index 6dcd13c..eeaca6b 100644 --- a/handler_test.go +++ b/handler_test.go @@ -1,621 +1,1138 @@ -package aicra_test - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/xdrm-io/aicra" - "github.com/xdrm-io/aicra/api" - "github.com/xdrm-io/aicra/validator" -) - -func addBuiltinTypes(b *aicra.Builder) error { - if err := b.Validate(validator.AnyType{}); err != nil { - return err - } - if err := b.Validate(validator.BoolType{}); err != nil { - return err - } - if err := b.Validate(validator.FloatType{}); err != nil { - return err - } - if err := b.Validate(validator.IntType{}); err != nil { - return err - } - if err := b.Validate(validator.StringType{}); err != nil { - return err - } - if err := b.Validate(validator.UintType{}); err != nil { - return err - } - return nil -} - -func TestWith(t *testing.T) { - builder := &aicra.Builder{} - if err := addBuiltinTypes(builder); err != nil { - t.Fatalf("unexpected error <%v>", err) - } - - // build @n middlewares that take data from context and increment it - n := 1024 - - type ckey int - const key ckey = 0 - - middleware := func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // first time -> store 1 - value := r.Context().Value(key) - if value == nil { - r = r.WithContext(context.WithValue(r.Context(), key, int(1))) - next.ServeHTTP(w, r) - return - } - - // get value and increment - cast, ok := value.(int) - if !ok { - t.Fatalf("value is not an int") - } - cast++ - r = r.WithContext(context.WithValue(r.Context(), key, cast)) - next.ServeHTTP(w, r) - }) - } - - // add middleware @n times - for i := 0; i < n; i++ { - builder.With(middleware) - } - - config := strings.NewReader(`[ { "method": "GET", "path": "/path", "scope": [[]], "info": "info", "in": {}, "out": {} } ]`) - err := builder.Setup(config) - if err != nil { - t.Fatalf("setup: unexpected error <%v>", err) - } - - pathHandler := func(ctx context.Context) (*struct{}, api.Err) { - // write value from middlewares into response - value := ctx.Value(key) - if value == nil { - t.Fatalf("nothing found in context") - } - cast, ok := value.(int) - if !ok { - t.Fatalf("cannot cast context data to int") - } - // write to response - api.GetResponseWriter(ctx).Write([]byte(fmt.Sprintf("#%d#", cast))) - - return nil, api.ErrSuccess - } - - if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { - t.Fatalf("bind: unexpected error <%v>", err) - } - - handler, err := builder.Build() - if err != nil { - t.Fatalf("build: unexpected error <%v>", err) - } - - response := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) - - // test request - handler.ServeHTTP(response, request) - if response.Body == nil { - t.Fatalf("response has no body") - } - token := fmt.Sprintf("#%d#", n) - if !strings.Contains(response.Body.String(), token) { - t.Fatalf("expected '%s' to be in response <%s>", token, response.Body.String()) - } - -} - -func TestWithAuth(t *testing.T) { - - tt := []struct { - name string - manifest string - permissions []string - granted bool - }{ - { - name: "provide only requirement A", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A"}, - granted: true, - }, - { - name: "missing requirement", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{}, - granted: false, - }, - { - name: "missing requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{}, - granted: false, - }, - { - name: "missing some requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A"}, - granted: false, - }, - { - name: "provide requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A", "B"}, - granted: true, - }, - { - name: "missing OR requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"C"}, - granted: false, - }, - { - name: "provide 1 OR requirement", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A"}, - granted: true, - }, - { - name: "provide both OR requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A", "B"}, - granted: true, - }, - { - name: "missing composite OR requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{}, - granted: false, - }, - { - name: "missing partial composite OR requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A", "C"}, - granted: false, - }, - { - name: "provide 1 composite OR requirement", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A", "B", "C"}, - granted: true, - }, - { - name: "provide both composite OR requirements", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A", "B", "C", "D"}, - granted: true, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - builder := &aicra.Builder{} - if err := addBuiltinTypes(builder); err != nil { - t.Fatalf("unexpected error <%v>", err) - } - - // tester middleware (last executed) - builder.WithContext(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - a := api.GetAuth(r.Context()) - if a == nil { - t.Fatalf("cannot access api.Auth form request context") - } - - if a.Granted() == tc.granted { - return - } - if a.Granted() { - t.Fatalf("unexpected granted auth") - } else { - t.Fatalf("expected granted auth") - } - next.ServeHTTP(w, r) - }) - }) - - builder.WithContext(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - a := api.GetAuth(r.Context()) - if a == nil { - t.Fatalf("cannot access api.Auth form request context") - } - - a.Active = tc.permissions - next.ServeHTTP(w, r) - }) - }) - - err := builder.Setup(strings.NewReader(tc.manifest)) - if err != nil { - t.Fatalf("setup: unexpected error <%v>", err) - } - - pathHandler := func(ctx context.Context) (*struct{}, api.Err) { - return nil, api.ErrNotImplemented - } - - if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { - t.Fatalf("bind: unexpected error <%v>", err) - } - - handler, err := builder.Build() - if err != nil { - t.Fatalf("build: unexpected error <%v>", err) - } - - response := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) - - // test request - handler.ServeHTTP(response, request) - if response.Body == nil { - t.Fatalf("response has no body") - } - - }) - } - -} - -func TestPermissionError(t *testing.T) { - - tt := []struct { - name string - manifest string - permissions []string - granted bool - }{ - { - name: "permission fulfilled", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{"A"}, - granted: true, - }, - { - name: "missing permission", - manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, - permissions: []string{}, - granted: false, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - builder := &aicra.Builder{} - if err := addBuiltinTypes(builder); err != nil { - t.Fatalf("unexpected error <%v>", err) - } - - // add active permissions - builder.WithContext(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - a := api.GetAuth(r.Context()) - if a == nil { - t.Fatalf("cannot access api.Auth form request context") - } - - a.Active = tc.permissions - next.ServeHTTP(w, r) - }) - }) - - err := builder.Setup(strings.NewReader(tc.manifest)) - if err != nil { - t.Fatalf("setup: unexpected error <%v>", err) - } - - pathHandler := func(ctx context.Context) (*struct{}, api.Err) { - return nil, api.ErrNotImplemented - } - - if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { - t.Fatalf("bind: unexpected error <%v>", err) - } - - handler, err := builder.Build() - if err != nil { - t.Fatalf("build: unexpected error <%v>", err) - } - - response := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) - - // test request - handler.ServeHTTP(response, request) - if response.Body == nil { - t.Fatalf("response has no body") - } - type jsonResponse struct { - Err api.Err `json:"error"` - } - var res jsonResponse - err = json.Unmarshal(response.Body.Bytes(), &res) - if err != nil { - t.Fatalf("cannot unmarshal response: %s", err) - } - - expectedError := api.ErrNotImplemented - if !tc.granted { - expectedError = api.ErrPermission - } - - if res.Err.Code != expectedError.Code { - t.Fatalf("expected error code %d got %d", expectedError.Code, res.Err.Code) - } - - }) - } - -} - -func TestDynamicScope(t *testing.T) { - tt := []struct { - name string - manifest string - path string - handler interface{} - url string - body string - permissions []string - granted bool - }{ - { - name: "replace one granted", - manifest: `[ - { - "method": "POST", - "path": "/path/{id}", - "info": "info", - "scope": [["user[Input1]"]], - "in": { - "{id}": { "info": "info", "name": "Input1", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/path/{id}", - handler: func(context.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, - url: "/path/123", - body: ``, - permissions: []string{"user[123]"}, - granted: true, - }, - { - name: "replace one mismatch", - manifest: `[ - { - "method": "POST", - "path": "/path/{id}", - "info": "info", - "scope": [["user[Input1]"]], - "in": { - "{id}": { "info": "info", "name": "Input1", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/path/{id}", - handler: func(context.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, - url: "/path/666", - body: ``, - permissions: []string{"user[123]"}, - granted: false, - }, - { - name: "replace one valid dot separated", - manifest: `[ - { - "method": "POST", - "path": "/path/{id}", - "info": "info", - "scope": [["prefix.user[User].suffix"]], - "in": { - "{id}": { "info": "info", "name": "User", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/path/{id}", - handler: func(context.Context, struct{ User uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, - url: "/path/123", - body: ``, - permissions: []string{"prefix.user[123].suffix"}, - granted: true, - }, - { - name: "replace two valid dot separated", - manifest: `[ - { - "method": "POST", - "path": "/prefix/{pid}/user/{uid}", - "info": "info", - "scope": [["prefix[Prefix].user[User].suffix"]], - "in": { - "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, - "{uid}": { "info": "info", "name": "User", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/prefix/{pid}/user/{uid}", - handler: func(context.Context, struct { - Prefix uint - User uint - }) (*struct{}, api.Err) { - return nil, api.ErrSuccess - }, - url: "/prefix/123/user/456", - body: ``, - permissions: []string{"prefix[123].user[456].suffix"}, - granted: true, - }, - { - name: "replace two invalid dot separated", - manifest: `[ - { - "method": "POST", - "path": "/prefix/{pid}/user/{uid}", - "info": "info", - "scope": [["prefix[Prefix].user[User].suffix"]], - "in": { - "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, - "{uid}": { "info": "info", "name": "User", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/prefix/{pid}/user/{uid}", - handler: func(context.Context, struct { - Prefix uint - User uint - }) (*struct{}, api.Err) { - return nil, api.ErrSuccess - }, - url: "/prefix/123/user/666", - body: ``, - permissions: []string{"prefix[123].user[456].suffix"}, - granted: false, - }, - { - name: "replace three valid dot separated", - manifest: `[ - { - "method": "POST", - "path": "/prefix/{pid}/user/{uid}/suffix/{sid}", - "info": "info", - "scope": [["prefix[Prefix].user[User].suffix[Suffix]"]], - "in": { - "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, - "{uid}": { "info": "info", "name": "User", "type": "uint" }, - "{sid}": { "info": "info", "name": "Suffix", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/prefix/{pid}/user/{uid}/suffix/{sid}", - handler: func(context.Context, struct { - Prefix uint - User uint - Suffix uint - }) (*struct{}, api.Err) { - return nil, api.ErrSuccess - }, - url: "/prefix/123/user/456/suffix/789", - body: ``, - permissions: []string{"prefix[123].user[456].suffix[789]"}, - granted: true, - }, - { - name: "replace three invalid dot separated", - manifest: `[ - { - "method": "POST", - "path": "/prefix/{pid}/user/{uid}/suffix/{sid}", - "info": "info", - "scope": [["prefix[Prefix].user[User].suffix[Suffix]"]], - "in": { - "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, - "{uid}": { "info": "info", "name": "User", "type": "uint" }, - "{sid}": { "info": "info", "name": "Suffix", "type": "uint" } - }, - "out": {} - } - ]`, - path: "/prefix/{pid}/user/{uid}/suffix/{sid}", - handler: func(context.Context, struct { - Prefix uint - User uint - Suffix uint - }) (*struct{}, api.Err) { - return nil, api.ErrSuccess - }, - url: "/prefix/123/user/666/suffix/789", - body: ``, - permissions: []string{"prefix[123].user[456].suffix[789]"}, - granted: false, - }, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - builder := &aicra.Builder{} - if err := addBuiltinTypes(builder); err != nil { - t.Fatalf("unexpected error <%v>", err) - } - - // tester middleware (last executed) - builder.WithContext(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - a := api.GetAuth(r.Context()) - if a == nil { - t.Fatalf("cannot access api.Auth form request context") - } - if a.Granted() == tc.granted { - return - } - if a.Granted() { - t.Fatalf("unexpected granted auth") - } else { - t.Fatalf("expected granted auth") - } - next.ServeHTTP(w, r) - }) - }) - - // update permissions - builder.WithContext(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - a := api.GetAuth(r.Context()) - if a == nil { - t.Fatalf("cannot access api.Auth form request context") - } - a.Active = tc.permissions - next.ServeHTTP(w, r) - }) - }) - - err := builder.Setup(strings.NewReader(tc.manifest)) - if err != nil { - t.Fatalf("setup: unexpected error <%v>", err) - } - - if err := builder.Bind(http.MethodPost, tc.path, tc.handler); err != nil { - t.Fatalf("bind: unexpected error <%v>", err) - } - - handler, err := builder.Build() - if err != nil { - t.Fatalf("build: unexpected error <%v>", err) - } - - response := httptest.NewRecorder() - body := strings.NewReader(tc.body) - request := httptest.NewRequest(http.MethodPost, tc.url, body) - - // test request - handler.ServeHTTP(response, request) - if response.Body == nil { - t.Fatalf("response has no body") - } - - }) - } - -} +package aicra_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/xdrm-io/aicra" + "github.com/xdrm-io/aicra/api" + "github.com/xdrm-io/aicra/validator" +) + +func printEscaped(raw string) string { + raw = strings.ReplaceAll(raw, "\n", "\\n") + raw = strings.ReplaceAll(raw, "\r", "\\r") + return raw +} + +func addDefaultTypes(b *aicra.Builder) error { + if err := b.Validate(validator.AnyType{}); err != nil { + return err + } + if err := b.Validate(validator.BoolType{}); err != nil { + return err + } + if err := b.Validate(validator.FloatType{}); err != nil { + return err + } + if err := b.Validate(validator.IntType{}); err != nil { + return err + } + if err := b.Validate(validator.StringType{}); err != nil { + return err + } + if err := b.Validate(validator.UintType{}); err != nil { + return err + } + return nil +} + +func TestHandler_With(t *testing.T) { + builder := &aicra.Builder{} + if err := addDefaultTypes(builder); err != nil { + t.Fatalf("unexpected error <%v>", err) + } + + // build @n middlewares that take data from context and increment it + n := 1024 + + type ckey int + const key ckey = 0 + + middleware := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // first time -> store 1 + value := r.Context().Value(key) + if value == nil { + r = r.WithContext(context.WithValue(r.Context(), key, int(1))) + next.ServeHTTP(w, r) + return + } + + // get value and increment + cast, ok := value.(int) + if !ok { + t.Fatalf("value is not an int") + } + cast++ + r = r.WithContext(context.WithValue(r.Context(), key, cast)) + next.ServeHTTP(w, r) + }) + } + + // add middleware @n times + for i := 0; i < n; i++ { + builder.With(middleware) + } + + config := strings.NewReader(`[ { "method": "GET", "path": "/path", "scope": [[]], "info": "info", "in": {}, "out": {} } ]`) + err := builder.Setup(config) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + pathHandler := func(ctx context.Context) (*struct{}, api.Err) { + // write value from middlewares into response + value := ctx.Value(key) + if value == nil { + t.Fatalf("nothing found in context") + } + cast, ok := value.(int) + if !ok { + t.Fatalf("cannot cast context data to int") + } + // write to response + api.GetResponseWriter(ctx).Write([]byte(fmt.Sprintf("#%d#", cast))) + + return nil, api.ErrSuccess + } + + if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { + t.Fatalf("bind: unexpected error <%v>", err) + } + + handler, err := builder.Build() + if err != nil { + t.Fatalf("build: unexpected error <%v>", err) + } + + response := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) + + // test request + handler.ServeHTTP(response, request) + if response.Body == nil { + t.Fatalf("response has no body") + } + token := fmt.Sprintf("#%d#", n) + if !strings.Contains(response.Body.String(), token) { + t.Fatalf("expected '%s' to be in response <%s>", token, response.Body.String()) + } + +} + +func TestHandler_WithAuth(t *testing.T) { + + tt := []struct { + name string + manifest string + permissions []string + granted bool + }{ + { + name: "provide only requirement A", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A"}, + granted: true, + }, + { + name: "missing requirement", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{}, + granted: false, + }, + { + name: "missing requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{}, + granted: false, + }, + { + name: "missing some requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A"}, + granted: false, + }, + { + name: "provide requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A", "B"}, + granted: true, + }, + { + name: "missing OR requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"C"}, + granted: false, + }, + { + name: "provide 1 OR requirement", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A"}, + granted: true, + }, + { + name: "provide both OR requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"], ["B"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A", "B"}, + granted: true, + }, + { + name: "missing composite OR requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{}, + granted: false, + }, + { + name: "missing partial composite OR requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A", "C"}, + granted: false, + }, + { + name: "provide 1 composite OR requirement", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A", "B", "C"}, + granted: true, + }, + { + name: "provide both composite OR requirements", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A", "B"], ["C", "D"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A", "B", "C", "D"}, + granted: true, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + builder := &aicra.Builder{} + if err := addDefaultTypes(builder); err != nil { + t.Fatalf("unexpected error <%v>", err) + } + + // tester middleware (last executed) + builder.WithContext(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a := api.GetAuth(r.Context()) + if a == nil { + t.Fatalf("cannot access api.Auth form request context") + } + + if a.Granted() == tc.granted { + return + } + if a.Granted() { + t.Fatalf("unexpected granted auth") + } else { + t.Fatalf("expected granted auth") + } + next.ServeHTTP(w, r) + }) + }) + + builder.WithContext(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a := api.GetAuth(r.Context()) + if a == nil { + t.Fatalf("cannot access api.Auth form request context") + } + + a.Active = tc.permissions + next.ServeHTTP(w, r) + }) + }) + + err := builder.Setup(strings.NewReader(tc.manifest)) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + pathHandler := func(ctx context.Context) (*struct{}, api.Err) { + return nil, api.ErrNotImplemented + } + + if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { + t.Fatalf("bind: unexpected error <%v>", err) + } + + handler, err := builder.Build() + if err != nil { + t.Fatalf("build: unexpected error <%v>", err) + } + + response := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) + + // test request + handler.ServeHTTP(response, request) + if response.Body == nil { + t.Fatalf("response has no body") + } + + }) + } + +} + +func TestHandler_PermissionError(t *testing.T) { + + tt := []struct { + name string + manifest string + permissions []string + granted bool + }{ + { + name: "permission fulfilled", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{"A"}, + granted: true, + }, + { + name: "missing permission", + manifest: `[ { "method": "GET", "path": "/path", "scope": [["A"]], "info": "info", "in": {}, "out": {} } ]`, + permissions: []string{}, + granted: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + builder := &aicra.Builder{} + if err := addDefaultTypes(builder); err != nil { + t.Fatalf("unexpected error <%v>", err) + } + + // add active permissions + builder.WithContext(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a := api.GetAuth(r.Context()) + if a == nil { + t.Fatalf("cannot access api.Auth form request context") + } + + a.Active = tc.permissions + next.ServeHTTP(w, r) + }) + }) + + err := builder.Setup(strings.NewReader(tc.manifest)) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + pathHandler := func(ctx context.Context) (*struct{}, api.Err) { + return nil, api.ErrNotImplemented + } + + if err := builder.Bind(http.MethodGet, "/path", pathHandler); err != nil { + t.Fatalf("bind: unexpected error <%v>", err) + } + + handler, err := builder.Build() + if err != nil { + t.Fatalf("build: unexpected error <%v>", err) + } + + response := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/path", &bytes.Buffer{}) + + // test request + handler.ServeHTTP(response, request) + if response.Body == nil { + t.Fatalf("response has no body") + } + type jsonResponse struct { + Err api.Err `json:"error"` + } + var res jsonResponse + err = json.Unmarshal(response.Body.Bytes(), &res) + if err != nil { + t.Fatalf("cannot unmarshal response: %s", err) + } + + expectedError := api.ErrNotImplemented + if !tc.granted { + expectedError = api.ErrPermission + } + + if res.Err.Code != expectedError.Code { + t.Fatalf("expected error code %d got %d", expectedError.Code, res.Err.Code) + } + + }) + } + +} + +func TestHandler_DynamicScope(t *testing.T) { + tt := []struct { + name string + manifest string + path string + handler interface{} + url string + body string + permissions []string + granted bool + }{ + { + name: "replace one granted", + manifest: `[ + { + "method": "POST", + "path": "/path/{id}", + "info": "info", + "scope": [["user[Input1]"]], + "in": { + "{id}": { "info": "info", "name": "Input1", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/path/{id}", + handler: func(context.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + url: "/path/123", + body: ``, + permissions: []string{"user[123]"}, + granted: true, + }, + { + name: "replace one mismatch", + manifest: `[ + { + "method": "POST", + "path": "/path/{id}", + "info": "info", + "scope": [["user[Input1]"]], + "in": { + "{id}": { "info": "info", "name": "Input1", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/path/{id}", + handler: func(context.Context, struct{ Input1 uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + url: "/path/666", + body: ``, + permissions: []string{"user[123]"}, + granted: false, + }, + { + name: "replace one valid dot separated", + manifest: `[ + { + "method": "POST", + "path": "/path/{id}", + "info": "info", + "scope": [["prefix.user[User].suffix"]], + "in": { + "{id}": { "info": "info", "name": "User", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/path/{id}", + handler: func(context.Context, struct{ User uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + url: "/path/123", + body: ``, + permissions: []string{"prefix.user[123].suffix"}, + granted: true, + }, + { + name: "replace two valid dot separated", + manifest: `[ + { + "method": "POST", + "path": "/prefix/{pid}/user/{uid}", + "info": "info", + "scope": [["prefix[Prefix].user[User].suffix"]], + "in": { + "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, + "{uid}": { "info": "info", "name": "User", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/prefix/{pid}/user/{uid}", + handler: func(context.Context, struct { + Prefix uint + User uint + }) (*struct{}, api.Err) { + return nil, api.ErrSuccess + }, + url: "/prefix/123/user/456", + body: ``, + permissions: []string{"prefix[123].user[456].suffix"}, + granted: true, + }, + { + name: "replace two invalid dot separated", + manifest: `[ + { + "method": "POST", + "path": "/prefix/{pid}/user/{uid}", + "info": "info", + "scope": [["prefix[Prefix].user[User].suffix"]], + "in": { + "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, + "{uid}": { "info": "info", "name": "User", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/prefix/{pid}/user/{uid}", + handler: func(context.Context, struct { + Prefix uint + User uint + }) (*struct{}, api.Err) { + return nil, api.ErrSuccess + }, + url: "/prefix/123/user/666", + body: ``, + permissions: []string{"prefix[123].user[456].suffix"}, + granted: false, + }, + { + name: "replace three valid dot separated", + manifest: `[ + { + "method": "POST", + "path": "/prefix/{pid}/user/{uid}/suffix/{sid}", + "info": "info", + "scope": [["prefix[Prefix].user[User].suffix[Suffix]"]], + "in": { + "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, + "{uid}": { "info": "info", "name": "User", "type": "uint" }, + "{sid}": { "info": "info", "name": "Suffix", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/prefix/{pid}/user/{uid}/suffix/{sid}", + handler: func(context.Context, struct { + Prefix uint + User uint + Suffix uint + }) (*struct{}, api.Err) { + return nil, api.ErrSuccess + }, + url: "/prefix/123/user/456/suffix/789", + body: ``, + permissions: []string{"prefix[123].user[456].suffix[789]"}, + granted: true, + }, + { + name: "replace three invalid dot separated", + manifest: `[ + { + "method": "POST", + "path": "/prefix/{pid}/user/{uid}/suffix/{sid}", + "info": "info", + "scope": [["prefix[Prefix].user[User].suffix[Suffix]"]], + "in": { + "{pid}": { "info": "info", "name": "Prefix", "type": "uint" }, + "{uid}": { "info": "info", "name": "User", "type": "uint" }, + "{sid}": { "info": "info", "name": "Suffix", "type": "uint" } + }, + "out": {} + } + ]`, + path: "/prefix/{pid}/user/{uid}/suffix/{sid}", + handler: func(context.Context, struct { + Prefix uint + User uint + Suffix uint + }) (*struct{}, api.Err) { + return nil, api.ErrSuccess + }, + url: "/prefix/123/user/666/suffix/789", + body: ``, + permissions: []string{"prefix[123].user[456].suffix[789]"}, + granted: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + builder := &aicra.Builder{} + if err := addDefaultTypes(builder); err != nil { + t.Fatalf("unexpected error <%v>", err) + } + + // tester middleware (last executed) + builder.WithContext(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a := api.GetAuth(r.Context()) + if a == nil { + t.Fatalf("cannot access api.Auth form request context") + } + if a.Granted() == tc.granted { + return + } + if a.Granted() { + t.Fatalf("unexpected granted auth") + } else { + t.Fatalf("expected granted auth") + } + next.ServeHTTP(w, r) + }) + }) + + // update permissions + builder.WithContext(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + a := api.GetAuth(r.Context()) + if a == nil { + t.Fatalf("cannot access api.Auth form request context") + } + a.Active = tc.permissions + next.ServeHTTP(w, r) + }) + }) + + err := builder.Setup(strings.NewReader(tc.manifest)) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + if err := builder.Bind(http.MethodPost, tc.path, tc.handler); err != nil { + t.Fatalf("bind: unexpected error <%v>", err) + } + + handler, err := builder.Build() + if err != nil { + t.Fatalf("build: unexpected error <%v>", err) + } + + response := httptest.NewRecorder() + body := strings.NewReader(tc.body) + request := httptest.NewRequest(http.MethodPost, tc.url, body) + + // test request + handler.ServeHTTP(response, request) + if response.Body == nil { + t.Fatalf("response has no body") + } + + }) + } + +} + +func TestHandler_ServiceErrors(t *testing.T) { + tt := []struct { + name string + manifest string + // handler + hmethod, huri string + hfn interface{} + // request + method, url string + contentType string + body string + permissions []string + err api.Err + }{ + // service match + { + name: "unknown service method", + manifest: `[ + { + "method": "GET", + "path": "/", + "info": "info", + "scope": [], + "in": {}, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/", + hfn: func(context.Context) api.Err { + return api.ErrSuccess + }, + method: http.MethodPost, + url: "/", + body: ``, + permissions: []string{}, + err: api.ErrUnknownService, + }, + { + name: "unknown service path", + manifest: `[ + { + "method": "GET", + "path": "/", + "info": "info", + "scope": [], + "in": {}, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/", + hfn: func(context.Context) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/invalid", + body: ``, + permissions: []string{}, + err: api.ErrUnknownService, + }, + { + name: "valid empty service", + manifest: `[ + { + "method": "GET", + "path": "/", + "info": "info", + "scope": [], + "in": {}, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/", + hfn: func(context.Context) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/", + body: ``, + permissions: []string{}, + err: api.ErrSuccess, + }, + + // invalid uri param -> unknown service + { + name: "invalid uri param", + manifest: `[ + { + "method": "GET", + "path": "/a/{id}/b", + "info": "info", + "scope": [], + "in": { + "{id}": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/a/{id}/b", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/a/invalid/b", + body: ``, + permissions: []string{}, + err: api.ErrUnknownService, + }, + + // query param + { + name: "missing query param", + manifest: `[ + { + "method": "GET", + "path": "/", + "info": "info", + "scope": [], + "in": { + "GET@id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/", + body: ``, + permissions: []string{}, + err: api.ErrMissingParam, + }, + { + name: "invalid query param", + manifest: `[ + { + "method": "GET", + "path": "/a", + "info": "info", + "scope": [], + "in": { + "GET@id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/a", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/a?id=abc", + body: ``, + permissions: []string{}, + err: api.ErrInvalidParam, + }, + { + name: "invalid query multi param", + manifest: `[ + { + "method": "GET", + "path": "/a", + "info": "info", + "scope": [], + "in": { + "GET@id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/a", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/a?id=123&id=456", + body: ``, + permissions: []string{}, + err: api.ErrInvalidParam, + }, + { + name: "valid query param", + manifest: `[ + { + "method": "GET", + "path": "/a", + "info": "info", + "scope": [], + "in": { + "GET@id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodGet, + huri: "/a", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + method: http.MethodGet, + url: "/a?id=123", + body: ``, + permissions: []string{}, + err: api.ErrSuccess, + }, + + // json param + { + name: "missing json param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "application/json", + method: http.MethodPost, + url: "/", + body: ``, + permissions: []string{}, + err: api.ErrMissingParam, + }, + { + name: "invalid json param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "application/json", + method: http.MethodPost, + url: "/", + body: `{ "id": "invalid type" }`, + permissions: []string{}, + err: api.ErrInvalidParam, + }, + { + name: "valid json param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "application/json", + method: http.MethodPost, + url: "/", + body: `{ "id": 123 }`, + permissions: []string{}, + err: api.ErrSuccess, + }, + + // urlencoded param + { + name: "missing urlencoded param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "application/x-www-form-urlencoded", + method: http.MethodPost, + url: "/", + body: ``, + permissions: []string{}, + err: api.ErrMissingParam, + }, + { + name: "invalid urlencoded param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "application/x-www-form-urlencoded", + method: http.MethodPost, + url: "/", + body: `id=abc`, + permissions: []string{}, + err: api.ErrInvalidParam, + }, + { + name: "valid urlencoded param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "application/x-www-form-urlencoded", + method: http.MethodPost, + url: "/", + body: `id=123`, + permissions: []string{}, + err: api.ErrSuccess, + }, + + // formdata param + { + name: "missing multipart param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "multipart/form-data; boundary=xxx", + method: http.MethodPost, + url: "/", + body: ``, + permissions: []string{}, + err: api.ErrMissingParam, + }, + { + name: "invalid multipart param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "multipart/form-data; boundary=xxx", + method: http.MethodPost, + url: "/", + body: `--xxx +Content-Disposition: form-data; name="id" + +abc +--xxx--`, + permissions: []string{}, + err: api.ErrInvalidParam, + }, + { + name: "valid multipart param", + manifest: `[ + { + "method": "POST", + "path": "/", + "info": "info", + "scope": [], + "in": { + "id": { "info": "info", "type": "int", "name": "ID" } + }, + "out": {} + } + ]`, + hmethod: http.MethodPost, + huri: "/", + hfn: func(context.Context, struct{ ID int }) api.Err { + return api.ErrSuccess + }, + contentType: "multipart/form-data; boundary=xxx", + method: http.MethodPost, + url: "/", + body: `--xxx +Content-Disposition: form-data; name="id" + +123 +--xxx--`, + permissions: []string{}, + err: api.ErrSuccess, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + builder := &aicra.Builder{} + if err := addDefaultTypes(builder); err != nil { + t.Fatalf("unexpected error <%v>", err) + } + + err := builder.Setup(strings.NewReader(tc.manifest)) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + if err := builder.Bind(tc.hmethod, tc.huri, tc.hfn); err != nil { + t.Fatalf("bind: unexpected error <%v>", err) + } + + handler, err := builder.Build() + if err != nil { + t.Fatalf("build: unexpected error <%v>", err) + } + + var ( + response = httptest.NewRecorder() + body = strings.NewReader(tc.body) + request = httptest.NewRequest(tc.method, tc.url, body) + ) + if len(tc.contentType) > 0 { + request.Header.Add("Content-Type", tc.contentType) + } + + // test request + handler.ServeHTTP(response, request) + if response.Body == nil { + t.Fatalf("response has no body") + } + + jsonErr, err := json.Marshal(tc.err) + if err != nil { + t.Fatalf("cannot marshal expected error: %v", err) + } + jsonExpected := fmt.Sprintf(`{"error":%s}`, jsonErr) + if response.Body.String() != jsonExpected { + t.Fatalf("invalid response:\n- actual: %s\n- expect: %s\n", printEscaped(response.Body.String()), printEscaped(jsonExpected)) + } + }) + } +}