diff --git a/builder.go b/builder.go index f65fefa..224ce7c 100644 --- a/builder.go +++ b/builder.go @@ -26,17 +26,18 @@ type apiHandler struct { } // AddType adds an available datatype to the api definition -func (b *Builder) AddType(t datatype.T) { +func (b *Builder) AddType(t datatype.T) error { if b.conf == nil { b.conf = &config.Server{} } if b.conf.Services != nil { - panic(errLateType) + return errLateType } if b.conf.Types == nil { b.conf.Types = make([]datatype.T, 0) } b.conf.Types = append(b.conf.Types, t) + return nil } // Use adds an http adapter (middleware) diff --git a/builder_test.go b/builder_test.go new file mode 100644 index 0000000..8e3358d --- /dev/null +++ b/builder_test.go @@ -0,0 +1,352 @@ +package aicra + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.xdrm.io/go/aicra/api" + "git.xdrm.io/go/aicra/datatype/builtin" +) + +func addBuiltinTypes(b *Builder) error { + if err := b.AddType(builtin.AnyDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.BoolDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.FloatDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.IntDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.StringDataType{}); err != nil { + return err + } + if err := b.AddType(builtin.UintDataType{}); err != nil { + return err + } + return nil +} + +func TestAddType(t *testing.T) { + builder := &Builder{} + err := builder.AddType(builtin.BoolDataType{}) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + err = builder.Setup(strings.NewReader("[]")) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + err = builder.AddType(builtin.FloatDataType{}) + if err != errLateType { + t.Fatalf("expected <%v> got <%v>", errLateType, err) + } +} + +func TestUse(t *testing.T) { + builder := &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.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + newr := r + + // first time -> store 1 + value := r.Context().Value(key) + if value == nil { + newr = r.WithContext(context.WithValue(r.Context(), key, int(1))) + next(w, newr) + return + } + + // get value and increment + cast, ok := value.(int) + if !ok { + t.Fatalf("value is not an int") + } + cast++ + newr = r.WithContext(context.WithValue(r.Context(), key, cast)) + next(w, newr) + } + } + + // add middleware @n times + for i := 0; i < n; i++ { + builder.Use(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 api.Ctx) (*struct{}, api.Err) { + // write value from middlewares into response + value := ctx.Req.Context().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 + ctx.Res.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 TestBind(t *testing.T) { + tcases := []struct { + Name string + Config string + HandlerMethod string + HandlerPath string + HandlerFn interface{} // not bound if nil + BindErr error + BuildErr error + }{ + { + Name: "none required none provided", + Config: "[]", + HandlerMethod: "", + HandlerPath: "", + HandlerFn: nil, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "none required 1 provided", + Config: "[]", + HandlerMethod: "", + HandlerPath: "", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: errUnknownService, + BuildErr: nil, + }, + { + Name: "1 required none provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: "", + HandlerPath: "", + HandlerFn: nil, + BindErr: nil, + BuildErr: errMissingHandler, + }, + { + Name: "1 required wrong method provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: http.MethodPost, + HandlerPath: "/path", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: errUnknownService, + BuildErr: errMissingHandler, + }, + { + Name: "1 required wrong path provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/paths", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: errUnknownService, + BuildErr: errMissingHandler, + }, + { + Name: "1 required valid provided", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": {}, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func() (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with int", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "int", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name int }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with uint", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "uint", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name uint }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with string", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "string", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name string }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + { + Name: "1 required with bool", + Config: `[ + { + "method": "GET", + "path": "/path", + "scope": [[]], + "info": "info", + "in": { + "id": { "info": "info", "type": "bool", "name": "Name" } + }, + "out": {} + } + ]`, + HandlerMethod: http.MethodGet, + HandlerPath: "/path", + HandlerFn: func(struct{ Name bool }) (*struct{}, api.Err) { return nil, api.ErrSuccess }, + BindErr: nil, + BuildErr: nil, + }, + } + + for _, tcase := range tcases { + t.Run(tcase.Name, func(t *testing.T) { + t.Parallel() + + builder := &Builder{} + + if err := addBuiltinTypes(builder); err != nil { + t.Fatalf("add built-in types: %s", err) + } + + err := builder.Setup(strings.NewReader(tcase.Config)) + if err != nil { + t.Fatalf("setup: unexpected error <%v>", err) + } + + if tcase.HandlerFn != nil { + err := builder.Bind(tcase.HandlerMethod, tcase.HandlerPath, tcase.HandlerFn) + if !errors.Is(err, tcase.BindErr) { + t.Fatalf("bind: expected <%v> got <%v>", tcase.BindErr, err) + } + } + + _, err = builder.Build() + if !errors.Is(err, tcase.BuildErr) { + t.Fatalf("build: expected <%v> got <%v>", tcase.BuildErr, err) + } + + }) + } +}