diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2b2e943 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2021 Harran Ali +Copyright (c) 2024 Zeni Kim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..04e818c --- /dev/null +++ b/cache.go @@ -0,0 +1,73 @@ +package core + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + "github.com/redis/go-redis/v9" +) + +var ctx context.Context + +type Cache struct { + redis *redis.Client +} + +func NewCache(cacheConfig CacheConfig) *Cache { + ctx = context.Background() + dbStr := os.Getenv("REDIS_DB") + db64, err := strconv.ParseInt(dbStr, 10, 64) + if err != nil { + panic(fmt.Sprintf("error parsing redis db env var: %v", err)) + } + db := int(db64) + rdb := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%v:%v", os.Getenv("REDIS_HOST"), os.Getenv("REDIS_PORT")), + Password: os.Getenv("REDIS_PASSWORD"), // no password set + DB: db, // use default DB + }) + + _, err = rdb.Ping(ctx).Result() + if cacheConfig.EnableCache && err != nil { + panic(fmt.Sprintf("problem connecting to redis cache, (if it's not needed you can disable it in config/cache.go): %v", err)) + } + + return &Cache{ + redis: rdb, + } +} + +func (c *Cache) Set(key string, value string) error { + err := c.redis.Set(ctx, key, value, 0).Err() + if err != nil { + return err + } + return nil +} + +func (c *Cache) SetWithExpiration(key string, value string, expiration time.Duration) error { + err := c.redis.Set(ctx, key, value, expiration).Err() + if err != nil { + return err + } + return nil +} + +func (c *Cache) Get(key string) (string, error) { + result, err := c.redis.Get(ctx, key).Result() + if err != nil { + return "", err + } + return result, nil +} + +func (c *Cache) Delete(key string) error { + err := c.redis.Del(ctx, key).Err() + if err != nil { + return err + } + return nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..f214e84 --- /dev/null +++ b/config.go @@ -0,0 +1,27 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +type EnvFileConfig struct { + UseDotEnvFile bool +} + +type RequestConfig struct { + MaxUploadFileSize int +} + +type JWTConfig struct { + SecretKey string + Lifetime int +} + +type GormConfig struct { + EnableGorm bool +} + +type CacheConfig struct { + EnableCache bool +} diff --git a/consts.go b/consts.go new file mode 100644 index 0000000..5669217 --- /dev/null +++ b/consts.go @@ -0,0 +1,17 @@ +package core + +const GET string = "get" +const POST string = "post" +const DELETE string = "delete" +const PATCH string = "patch" +const PUT string = "put" +const OPTIONS string = "options" +const HEAD string = "head" +const CONTENT_TYPE string = "content-Type" +const CONTENT_TYPE_HTML string = "text/html; charset=utf-8" +const CONTENT_TYPE_JSON string = "application/json" +const CONTENT_TYPE_TEXT string = "text/plain" +const CONTENT_TYPE_MULTIPART_FORM_DATA string = "multipart/form-data;" +const LOCALHOST string = "http://localhost" +const TEST_STR string = "Testing!" +const PRODUCTION string = "production" diff --git a/context.go b/context.go new file mode 100644 index 0000000..30c750b --- /dev/null +++ b/context.go @@ -0,0 +1,334 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "reflect" + "strconv" + "strings" + "syscall" + + "git.smarteching.com/goffee/core/logger" + "gorm.io/gorm" +) + +type Context struct { + Request *Request + Response *Response + GetValidator func() *Validator + GetJWT func() *JWT + GetGorm func() *gorm.DB + GetCache func() *Cache + GetHashing func() *Hashing + GetMailer func() *Mailer + GetEventsManager func() *EventsManager + GetLogger func() *logger.Logger +} + +// TODO enhance +func (c *Context) DebugAny(variable interface{}) { + var formatted string + m := reflect.ValueOf(variable) + if m.Kind() == reflect.Pointer { + formatted = fmt.Sprintf("\n\nType: [pointer] %v (%v) \nMemory Address: %v \nValue: %v\n\n", m.Type(), m.Elem().Kind(), m, m.Elem()) + } else { + formatted = fmt.Sprintf("\n\nType: %v (%v) \nValue: %v\n\n", m.Type(), m.Kind(), variable) + } + fmt.Println(formatted) + c.Response.HttpResponseWriter.Write([]byte(formatted)) +} + +func (c *Context) Next() { + ResolveApp().Next(c) +} + +func (c *Context) prepare(ctx *Context) { + ctx.Request.httpRequest.ParseMultipartForm(int64(app.Config.Request.MaxUploadFileSize)) +} + +func (c *Context) GetPathParam(key string) interface{} { + return c.Request.httpPathParams.ByName(key) +} + +func (c *Context) GetRequestParam(key string) interface{} { + return c.Request.httpRequest.FormValue(key) +} + +func (c *Context) RequestParamExists(key string) bool { + return c.Request.httpRequest.Form.Has(key) +} + +func (c *Context) GetHeader(key string) string { + return c.Request.httpRequest.Header.Get(key) +} + +func (c *Context) GetUploadedFile(name string) *UploadedFileInfo { + file, fileHeader, err := c.Request.httpRequest.FormFile(name) + if err != nil { + panic(fmt.Sprintf("error with file,[%v]", err.Error())) + } + defer file.Close() + ext := strings.TrimPrefix(path.Ext(fileHeader.Filename), ".") + tmpFilePath := filepath.Join(os.TempDir(), fileHeader.Filename) + tmpFile, err := os.Create(tmpFilePath) + if err != nil { + panic(fmt.Sprintf("error with file,[%v]", err.Error())) + } + buff := make([]byte, 100) + for { + n, err := file.Read(buff) + if err != nil && err != io.EOF { + panic("error with uploaded file") + } + if n == 0 { + break + } + n, _ = tmpFile.Write(buff[:n]) + } + tmpFileInfo, err := os.Stat(tmpFilePath) + if err != nil { + panic(fmt.Sprintf("error with file,[%v]", err.Error())) + } + defer tmpFile.Close() + uploadedFileInfo := &UploadedFileInfo{ + FullPath: tmpFilePath, + Name: fileHeader.Filename, + NameWithoutExtension: strings.TrimSuffix(fileHeader.Filename, path.Ext(fileHeader.Filename)), + Extension: ext, + Size: int(tmpFileInfo.Size()), + } + return uploadedFileInfo +} + +func (c *Context) MoveFile(sourceFilePath string, destFolderPath string) error { + o := syscall.Umask(0) + defer syscall.Umask(o) + newFileName := filepath.Base(sourceFilePath) + os.MkdirAll(destFolderPath, 766) + srcFileInfo, err := os.Stat(sourceFilePath) + if err != nil { + return err + } + if !srcFileInfo.Mode().IsRegular() { + return errors.New("can not move file, not in a regular mode") + } + srcFile, err := os.Open(sourceFilePath) + if err != nil { + return err + } + defer srcFile.Close() + destFilePath := filepath.Join(destFolderPath, newFileName) + destFile, err := os.Create(destFilePath) + if err != nil { + return err + } + buff := make([]byte, 1024*8) + for { + n, err := srcFile.Read(buff) + if err != nil && err != io.EOF { + panic(fmt.Sprintf("error moving file %v", sourceFilePath)) + } + if n == 0 { + break + } + _, err = destFile.Write(buff[:n]) + if err != nil { + return err + } + } + destFile.Close() + err = os.Remove(sourceFilePath) + if err != nil { + return err + } + return nil +} + +func (c *Context) CopyFile(sourceFilePath string, destFolderPath string) error { + o := syscall.Umask(0) + defer syscall.Umask(o) + newFileName := filepath.Base(sourceFilePath) + os.MkdirAll(destFolderPath, 766) + srcFileInfo, err := os.Stat(sourceFilePath) + if err != nil { + return err + } + if !srcFileInfo.Mode().IsRegular() { + return errors.New("can not move file, not in a regular mode") + } + srcFile, err := os.Open(sourceFilePath) + if err != nil { + return err + } + defer srcFile.Close() + destFilePath := filepath.Join(destFolderPath, newFileName) + destFile, err := os.Create(destFilePath) + if err != nil { + return err + } + buff := make([]byte, 1024*8) + for { + n, err := srcFile.Read(buff) + if err != nil && err != io.EOF { + panic(fmt.Sprintf("error moving file %v", sourceFilePath)) + } + if n == 0 { + break + } + _, err = destFile.Write(buff[:n]) + if err != nil { + return err + } + } + destFile.Close() + + return nil +} + +func (c *Context) MapToJson(v any) string { + r := reflect.ValueOf(v) + if r.Kind() != reflect.Map { + panic("parameter is not a map") + } + j, err := json.Marshal(v) + if err != nil { + panic(err.Error()) + } + return string(j) +} + +type UploadedFileInfo struct { + FullPath string + Name string + NameWithoutExtension string + Extension string + Size int +} + +func (c *Context) GetBaseDirPath() string { + return basePath +} + +func (c *Context) CastToString(value interface{}) string { + if !basicType(value) { + panic("can not cast to string") + } + + return fmt.Sprintf("%v", value) +} + +func (c Context) GetUserAgent() string { + return c.Request.httpRequest.UserAgent() +} + +func (c *Context) CastToInt(value interface{}) int { + var i int + if !basicType(value) { + panic("can not cast to int") + } + i, ok := value.(int) + if ok { + return i + } + _i, ok := value.(int32) + if ok { + i := int(_i) + return i + } + _ii, ok := value.(int64) + if ok { + i := int(_ii) + return i + } + f, ok := value.(float32) + if ok { + i := int(f) + return i + } + ff, ok := value.(float64) + if ok { + i := int(ff) + return i + } + s, ok := value.(string) + if ok { + fff, err := strconv.ParseFloat(s, 64) + if err != nil { + panic("error casting to int") + } + i = int(fff) + return i + } + panic("error casting to int") +} + +func (c *Context) CastToFloat(value interface{}) float64 { + if !basicType(value) { + panic("can not cast to float") + } + v := reflect.ValueOf(value) + if v.Kind() == reflect.Pointer { + v = v.Elem() + } + var str string + var ok bool + if v.Kind() == reflect.Float64 { + f, ok := value.(float64) + if !ok { + panic("error casting to float") + } + return f + } + if v.Kind() == reflect.Float32 { + s := fmt.Sprintf("%v", value) + f, err := strconv.ParseFloat(s, 64) + if err != nil { + panic("error casting to float") + } + return f + } + if v.Kind() == reflect.String { + str, ok = value.(string) + if !ok { + panic("error casting to float") + } + } + if v.CanInt() { + i, ok := value.(int) + if !ok { + panic("error casting to float") + } + str = fmt.Sprintf("%v.0", i) + } + f, err := strconv.ParseFloat(str, 64) + if err != nil { + panic("error casting to float") + } + return f +} + +func basicType(value interface{}) bool { + v := reflect.ValueOf(value) + if v.Kind() == reflect.Pointer { + v = v.Elem() + } + if !(v.Kind() == reflect.Array || + v.Kind() == reflect.Slice || + v.Kind() == reflect.Map || + v.Kind() == reflect.Struct || + v.Kind() == reflect.Interface || + v.Kind() == reflect.Func) { + return true + } + return false +} diff --git a/context_test.go b/context_test.go new file mode 100644 index 0000000..9cc4287 --- /dev/null +++ b/context_test.go @@ -0,0 +1,555 @@ +package core + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "syscall" + "testing" + + "git.smarteching.com/goffee/core/logger" + "github.com/google/uuid" + "github.com/julienschmidt/httprouter" +) + +func TestDebugAny(t *testing.T) { + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + c := &Context{ + Request: &Request{ + httpRequest: r, + httpPathParams: nil, + }, + Response: &Response{ + headers: []header{}, + body: nil, + HttpResponseWriter: w, + }, + GetValidator: nil, + GetJWT: nil, + } + h := func(c *Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var msg interface{} + msg = "test-debug-pointer" + c.DebugAny(&msg) + c.DebugAny("test-debug-msg") + } + }(c) + h(w, r) + b, err := io.ReadAll(w.Body) + if err != nil { + t.Errorf("failed testing debug any") + } + if !strings.Contains(string(b), "test-debug-msg") { + t.Errorf("failed testing debug any") + } + if !strings.Contains(string(b), "test-debug-pointer") { + t.Errorf("failed testing debug any") + } +} + +func TestLogInfo(t *testing.T) { + tmpF := filepath.Join(t.TempDir(), uuid.NewString()) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + msg := "test-log-info" + c := makeCTXLogTestCTX(t, w, r, tmpF) + h := func(c *Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + c.GetLogger().Info(msg) + } + }(c) + h(w, r) + fc, err := os.ReadFile(tmpF) + if err != nil { + t.Errorf("failed testing log info") + } + if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "info:")) { + t.Errorf("failed testing log info") + } +} + +func TestLogWarning(t *testing.T) { + tmpF := filepath.Join(t.TempDir(), uuid.NewString()) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + msg := "test-log-warning" + c := makeCTXLogTestCTX(t, w, r, tmpF) + h := func(c *Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + c.GetLogger().Warning(msg) + } + }(c) + h(w, r) + fc, err := os.ReadFile(tmpF) + if err != nil { + t.Errorf("failed testing log warning") + } + if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "warning:")) { + t.Errorf("failed testing log warning") + } +} + +func TestLogDebug(t *testing.T) { + tmpF := filepath.Join(t.TempDir(), uuid.NewString()) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + msg := "test-log-debug" + c := makeCTXLogTestCTX(t, w, r, tmpF) + h := func(c *Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + c.GetLogger().Debug(msg) + } + }(c) + h(w, r) + fc, err := os.ReadFile(tmpF) + if err != nil { + t.Errorf("failed testing log debug") + } + if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "debug:")) { + t.Errorf("failed testing log debug") + } +} + +func TestLogError(t *testing.T) { + tmpF := filepath.Join(t.TempDir(), uuid.NewString()) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + msg := "test-log-error" + c := makeCTXLogTestCTX(t, w, r, tmpF) + h := func(c *Context) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + c.GetLogger().Error(msg) + } + }(c) + h(w, r) + fc, err := os.ReadFile(tmpF) + if err != nil { + t.Errorf("failed testing log error") + } + if !(strings.Contains(string(fc), msg) || strings.Contains(string(fc), "error:")) { + t.Errorf("failed testing log error") + } +} + +func TestGetPathParams(t *testing.T) { + NewEventsManager() //TODO removing require refactoring makeHTTPRouterHandlerFunc() + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + pathParams := httprouter.Params{ + { + Key: "param1", + Value: "param1val", + }, + { + Key: "param2", + Value: "param2val", + }, + } + a := New() + h := a.makeHTTPRouterHandlerFunc( + Handler(func(c *Context) *Response { + rsp := fmt.Sprintf("param1: %v | param2: %v", c.GetPathParam("param1"), c.GetPathParam("param2")) + return c.Response.Text(rsp) + }), nil) + h(w, r, pathParams) + b, err := io.ReadAll(w.Body) + if err != nil { + t.Log("failed testing get path params") + } + bStr := string(b) + if !(strings.Contains(bStr, "param1val") || strings.Contains(bStr, "param2val")) { + t.Errorf("failed testing get path params") + } +} + +func TestGetRequestParams(t *testing.T) { + pwd, _ := os.Getwd() + app := New() + app.SetBasePath(pwd) + hr := httprouter.New() + gcr := NewRouter() + gcr.Post("/pt", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Get("/gt", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + client := s.Client() + defer s.Close() + rsp, err := client.PostForm(s.URL+"/pt", url.Values{"param": {"paramValPost"}}) + if err != nil { + t.Errorf("failed test get request params: %v", err) + } + + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Logf("failed test get request params") + } + if strings.TrimSpace(string(b)) != "paramValPost" { + t.Errorf("failed test get request params") + } + rsp.Body.Close() + rsp, err = http.Get(s.URL + "/gt?param=paramValGet") + b, err = io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test get request params") + } + if strings.TrimSpace(string(b)) != "paramValGet" { + t.Errorf("failed test get request param") + } + rsp.Body.Close() +} + +func TestRequestParamsExists(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Post("/pt", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param")) + return nil + })) + gcr.Get("/gt", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.RequestParamExists("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + rsp, err := http.PostForm(s.URL+"/pt", url.Values{"param": {"paramValPost"}}) + if err != nil { + t.Logf("failed test get request params") + } + + b, err := io.ReadAll(rsp.Body) + + if err != nil { + t.Logf("failed test get request params") + } + if strings.TrimSpace(string(b)) != "true" { + t.Errorf("failed test get request params") + } + rsp.Body.Close() + + rsp, err = http.Get(s.URL + "/gt?param=paramValGet") + b, err = io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test get request params") + } + if strings.TrimSpace(string(b)) != "true" { + t.Errorf("failed test get request param") + } + rsp.Body.Close() +} + +func TestGetHeader(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Post("/pt", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey")) + return nil + })) + gcr.Get("/gt", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetHeader("headerkey")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("POST", s.URL+"/pt", nil) + if err != nil { + t.Errorf("failed test get header") + } + req.Header.Add("headerkey", "headerPostVal") + rsp, err := clt.Do(req) + if err != nil { + t.Logf("failed test get request params") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Logf("failed test get request params") + } + if strings.TrimSpace(string(b)) != "headerPostVal" { + t.Errorf("failed test get request params") + } + req, err = http.NewRequest("GET", s.URL+"/gt", nil) + if err != nil { + t.Errorf("failed test get header") + } + req.Header.Add("headerkey", "headerGetVal") + rsp, err = clt.Do(req) + if err != nil { + t.Logf("failed test get request params") + } + b, err = io.ReadAll(rsp.Body) + if err != nil { + t.Logf("failed test get request params") + } + if strings.TrimSpace(string(b)) != "headerGetVal" { + t.Errorf("failed test get request params") + } +} + +func TestGetUploadedFile(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Post("/pt", Handler(func(c *Context) *Response { + uploadedFile := c.GetUploadedFile("myfile") + rs := fmt.Sprintf("file name: %v | size: %v", uploadedFile.Name, uploadedFile.Size) + fmt.Fprintln(c.Response.HttpResponseWriter, rs) + return nil + })) + + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + wd, _ := os.Getwd() + tfp := filepath.Join(wd, "testingdata/testdata.json") + file, err := os.Open(tfp) + if err != nil { + t.Error("failed test get upload file") + } + defer file.Close() + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("myfile", filepath.Base(file.Name())) + io.Copy(part, file) + writer.Close() + + clt := &http.Client{} + req, err := http.NewRequest("POST", s.URL+"/pt", body) + if err != nil { + t.Errorf("failed test get uploaded file") + } + req.Header.Add(CONTENT_TYPE, writer.FormDataContentType()) + rsp, err := clt.Do(req) + if err != nil { + t.Logf("failed test get get uploaded file") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Logf("failed test get get uploaded file") + } + rsp.Body.Close() + asrtFile, err := os.Stat(tfp) + if err != nil { + t.Logf("failed test get get uploaded file") + } + if !strings.Contains(string(b), fmt.Sprintf("size: %v", asrtFile.Size())) { + t.Errorf("failed test get uploaded file") + } + if !strings.Contains(string(b), "testdata.json") { + t.Errorf("failed test get uploaded file") + } +} + +func TestMoveFile(t *testing.T) { + o := syscall.Umask(0) + defer syscall.Umask(o) + pwd, _ := os.Getwd() + var tmpDir string + tmpDir = path.Join(pwd, "testingdata/tmp") + _, err := os.Stat(path.Join("./testingdata", "totestmovefile.md")) + if err != nil { + t.Errorf("failed test move file: %v", err.Error()) + } + + c := makeCTX(t) + err = c.MoveFile("./testingdata/totestmovefile.md", tmpDir) + if err != nil { + t.Errorf("failed test move file: %v", err.Error()) + } + + _, err = os.Stat(path.Join(tmpDir, "totestmovefile.md")) + if err != nil { + t.Errorf("failed test move file: %v", err.Error()) + } + t.Cleanup(func() { + c.MoveFile(filepath.Join(tmpDir, "totestmovefile.md"), "./testingdata") + }) +} + +func TestCopyFile(t *testing.T) { + o := syscall.Umask(0) + defer syscall.Umask(o) + pwd, _ := os.Getwd() + var tmpDir string + tmpDir = path.Join(pwd, "testingdata/tmp") + _, err := os.Stat(path.Join("./testingdata", "totestcopyfile.md")) + if err != nil { + t.Errorf("failed test move file: %v", err.Error()) + } + + c := makeCTX(t) + err = c.CopyFile("./testingdata/totestcopyfile.md", tmpDir) + if err != nil { + t.Errorf("failed test move file: %v", err.Error()) + } + + _, err = os.Stat(path.Join(tmpDir, "totestcopyfile.md")) + if err != nil { + t.Errorf("failed test move file: %v", err.Error()) + } + t.Cleanup(func() { + os.Remove(filepath.Join(tmpDir, "totestcopyfile.md")) + }) +} + +func TestCastToString(t *testing.T) { + c := makeCTX(t) + s := c.CastToString(25) + if fmt.Sprintf("%T", s) != "string" { + t.Errorf("failed test cast to string") + } + s = c.CastToString(25.54) + if fmt.Sprintf("%T", s) != "string" { + t.Errorf("failed test cast to string") + } + var v interface{} = "434" + s = c.CastToString(v) + if fmt.Sprintf("%T", s) != "string" { + t.Errorf("failed test cast to string") + } + var vs interface{} = "this is a string" + s = c.CastToString(vs) + if fmt.Sprintf("%T", s) != "string" { + t.Errorf("failed test cast to string") + } +} + +func TestCastToInt(t *testing.T) { + c := makeCTX(t) + i := c.CastToInt(4) + if !(i == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + ii := c.CastToInt(4.434) + if !(ii == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + iii := c.CastToInt("4") + if !(iii == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + iiii := c.CastToInt("4.434") + if !(iiii == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + var iInterface interface{} + iInterface = 4 + i = c.CastToInt(iInterface) + if !(i == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + iInterface = 4.545 + ii = c.CastToInt(iInterface) + if !(ii == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + iInterface = "4" + iii = c.CastToInt(iInterface) + if !(iii == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } + iInterface = "4.434" + iiii = c.CastToInt(iInterface) + if !(iiii == 4 && fmt.Sprintf("%T", i) == "int") { + t.Errorf("failed test cast to int") + } +} + +func TestCastToFloat(t *testing.T) { + c := makeCTX(t) + f := c.CastToFloat(4) + if !(f == 4 && fmt.Sprintf("%T", f) == "float64") { + t.Errorf("failed test cast to float") + } + var varf32 float32 = 4.434 + ff32 := c.CastToFloat(varf32) + if !(ff32 == 4.434 && fmt.Sprintf("%T", ff32) == "float64") { + t.Errorf("failed test cast to float") + } + var varf64 float64 = 4.434 + ff64 := c.CastToFloat(varf64) + if !(ff64 == 4.434 && fmt.Sprintf("%T", ff64) == "float64") { + t.Errorf("failed test cast to float") + } + + fff := c.CastToFloat("4") + if !(fff == 4 && fmt.Sprintf("%T", fff) == "float64") { + t.Errorf("failed test cast to float") + } + ffff := c.CastToFloat("4.434") + if !(ffff == 4.434 && fmt.Sprintf("%T", ffff) == "float64") { + t.Errorf("failed test cast to float") + } + var iInterface interface{} + iInterface = 4 + f = c.CastToFloat(iInterface) + if !(f == 4 && fmt.Sprintf("%T", f) == "float64") { + t.Errorf("failed test cast to float") + } + iInterface = 4.434 + iff := c.CastToFloat(iInterface) + if !(iff == 4.434 && fmt.Sprintf("%T", iff) == "float64") { + t.Errorf("failed test cast to float") + } + iInterface = "4" + fff = c.CastToFloat(iInterface) + if !(fff == 4 && fmt.Sprintf("%T", fff) == "float64") { + t.Errorf("failed test cast to float") + } + iInterface = "4.434" + ffff = c.CastToFloat(iInterface) + if !(ffff == 4.434 && fmt.Sprintf("%T", ffff) == "float64") { + t.Errorf("failed test cast to float") + } +} + +func TestGetBaseDirPath(t *testing.T) { + c := makeCTX(t) + p := c.GetBaseDirPath() + pwd, err := os.Getwd() + if err != nil { + t.Errorf("failed test get base dir path") + } + if p != pwd { + t.Errorf("failed test get base dir path") + } +} + +func makeCTXLogTestCTX(t *testing.T, w http.ResponseWriter, r *http.Request, tmpFilePath string) *Context { + t.Helper() + return &Context{ + Request: &Request{ + httpRequest: r, + httpPathParams: nil, + }, + Response: &Response{ + headers: []header{}, + body: nil, + HttpResponseWriter: w, + }, + GetValidator: nil, + GetJWT: nil, + GetLogger: func() *logger.Logger { + return logger.NewLogger(logger.LogFileDriver{FilePath: tmpFilePath}) + }, + } +} diff --git a/core.go b/core.go new file mode 100644 index 0000000..51212d4 --- /dev/null +++ b/core.go @@ -0,0 +1,541 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "fmt" + "log" + "net/http" + "os" + "path" + "path/filepath" + "runtime/debug" + "strconv" + "syscall" + + "git.smarteching.com/goffee/core/env" + "git.smarteching.com/goffee/core/logger" + "github.com/julienschmidt/httprouter" + "golang.org/x/crypto/acme/autocert" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var loggr *logger.Logger +var logsDriver *logger.LogsDriver +var requestC RequestConfig +var jwtC JWTConfig +var gormC GormConfig +var cacheC CacheConfig +var db *gorm.DB +var mailer *Mailer +var basePath string +var disableEvents bool = false + +type configContainer struct { + Request RequestConfig +} + +type App struct { + t int // for trancking middlewares + chain *chain + middlewares *Middlewares + Config *configContainer +} + +var app *App + +func New() *App { + app = &App{ + chain: &chain{}, + middlewares: NewMiddlewares(), + Config: &configContainer{ + Request: requestC, + }, + } + return app +} + +func ResolveApp() *App { + return app +} + +func (app *App) SetLogsDriver(d logger.LogsDriver) { + logsDriver = &d +} + +func (app *App) Bootstrap() { + loggr = logger.NewLogger(*logsDriver) + NewRouter() + NewEventsManager() +} + +func (app *App) Run(router *httprouter.Router) { + portNumber := os.Getenv("App_HTTP_PORT") + if portNumber == "" { + portNumber = "80" + } + router = app.RegisterRoutes(ResolveRouter().GetRoutes(), router) + useHttpsStr := os.Getenv("App_USE_HTTPS") + if useHttpsStr == "" { + useHttpsStr = "false" + } + useHttps, _ := strconv.ParseBool(useHttpsStr) + + fmt.Printf("Welcome to Goffee\n") + if useHttps { + fmt.Printf("Listening on https \nWaiting for requests...\n") + } else { + fmt.Printf("Listening on port %s\nWaiting for requests...\n", portNumber) + } + UseLetsEncryptStr := os.Getenv("App_USE_LETSENCRYPT") + if UseLetsEncryptStr == "" { + UseLetsEncryptStr = "false" + } + UseLetsEncrypt, _ := strconv.ParseBool(UseLetsEncryptStr) + if useHttps && UseLetsEncrypt { + m := &autocert.Manager{ + Cache: autocert.DirCache("letsencrypt-certs-dir"), + Prompt: autocert.AcceptTOS, + } + LetsEncryptEmail := os.Getenv("APP_LETSENCRYPT_EMAIL") + if LetsEncryptEmail != "" { + m.Email = LetsEncryptEmail + } + HttpsHosts := os.Getenv("App_HTTPS_HOSTS") + if HttpsHosts != "" { + m.HostPolicy = autocert.HostWhitelist(HttpsHosts) + } + log.Fatal(http.Serve(m.Listener(), router)) + return + } + if useHttps && !UseLetsEncrypt { + CertFile := os.Getenv("App_CERT_FILE_PATH") + if CertFile == "" { + CertFile = "tls/server.crt" + } + KeyFile := os.Getenv("App_KEY_FILE_PATH") + if KeyFile == "" { + KeyFile = "tls/server.key" + } + certFilePath := filepath.Join(basePath, CertFile) + KeyFilePath := filepath.Join(basePath, KeyFile) + log.Fatal(http.ListenAndServeTLS(":443", certFilePath, KeyFilePath, router)) + return + } + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", portNumber), router)) +} + +func (app *App) RegisterRoutes(routes []Route, router *httprouter.Router) *httprouter.Router { + router.PanicHandler = panicHandler + router.NotFound = notFoundHandler{} + router.MethodNotAllowed = methodNotAllowed{} + for _, route := range routes { + switch route.Method { + case GET: + router.GET(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + case POST: + router.POST(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + case DELETE: + router.DELETE(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + case PATCH: + router.PATCH(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + case PUT: + router.PUT(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + case OPTIONS: + router.OPTIONS(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + case HEAD: + router.HEAD(route.Path, app.makeHTTPRouterHandlerFunc(route.Handler, route.Middlewares)) + } + } + return router +} + +func (app *App) makeHTTPRouterHandlerFunc(h Handler, ms []Middleware) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + ctx := &Context{ + Request: &Request{ + httpRequest: r, + httpPathParams: ps, + }, + Response: &Response{ + headers: []header{}, + body: nil, + contentType: "", + overrideContentType: "", + HttpResponseWriter: w, + isTerminated: false, + redirectTo: "", + }, + GetValidator: getValidator(), + GetJWT: getJWT(), + GetGorm: getGormFunc(), + GetCache: resolveCache(), + GetHashing: resloveHashing(), + GetMailer: resolveMailer(), + GetEventsManager: resolveEventsManager(), + GetLogger: resolveLogger(), + } + ctx.prepare(ctx) + rhs := app.combHandlers(h, ms) + app.prepareChain(rhs) + app.t = 0 + app.chain.execute(ctx) + for _, header := range ctx.Response.headers { + w.Header().Add(header.key, header.val) + } + logger.CloseLogsFile() + var ct string + if ctx.Response.overrideContentType != "" { + ct = ctx.Response.overrideContentType + } else if ctx.Response.contentType != "" { + ct = ctx.Response.contentType + } else { + ct = CONTENT_TYPE_HTML + } + w.Header().Add(CONTENT_TYPE, ct) + if ctx.Response.statusCode != 0 { + w.WriteHeader(ctx.Response.statusCode) + } + if ctx.Response.redirectTo != "" { + http.Redirect(w, r, ctx.Response.redirectTo, http.StatusPermanentRedirect) + } else { + w.Write(ctx.Response.body) + } + e := ResolveEventsManager() + if e != nil { + e.setContext(ctx).processFiredEvents() + } + + app.t = 0 + ctx.Response.reset() + app.chain.reset() + } +} + +type notFoundHandler struct{} +type methodNotAllowed struct{} + +func (n notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + res := "{\"message\": \"Not Found\"}" + loggr.Error("Not Found") + loggr.Error(debug.Stack()) + w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON) + w.Write([]byte(res)) +} + +func (n methodNotAllowed) ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) + res := "{\"message\": \"Method not allowed\"}" + loggr.Error("Method not allowed") + loggr.Error(debug.Stack()) + w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON) + w.Write([]byte(res)) +} + +var panicHandler = func(w http.ResponseWriter, r *http.Request, e interface{}) { + isDebugModeStr := os.Getenv("APP_DEBUG_MODE") + isDebugMode, err := strconv.ParseBool(isDebugModeStr) + if err != nil { + errStr := "error parsing env var APP_DEBUG_MODE" + loggr.Error(errStr) + fmt.Sprintln(errStr) + w.Write([]byte(errStr)) + return + } + if !isDebugMode { + errStr := "internal error" + loggr.Error(errStr) + fmt.Sprintln(errStr) + w.WriteHeader(http.StatusInternalServerError) + w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON) + w.Write([]byte(fmt.Sprintf("{\"message\": \"%v\"}", errStr))) + return + } + shrtMsg := fmt.Sprintf("%v", e) + loggr.Error(shrtMsg) + fmt.Println(shrtMsg) + loggr.Error(string(debug.Stack())) + var res string + if env.GetVarOtherwiseDefault("APP_ENV", "local") == PRODUCTION { + res = "{\"message\": \"internal error\"}" + } else { + res = fmt.Sprintf("{\"message\": \"%v\", \"stack trace\": \"%v\"}", e, string(debug.Stack())) + } + w.WriteHeader(http.StatusInternalServerError) + w.Header().Add(CONTENT_TYPE, CONTENT_TYPE_JSON) + w.Write([]byte(res)) +} + +func UseMiddleware(mw Middleware) { + ResolveMiddlewares().Attach(mw) +} + +func (app *App) Next(c *Context) { + app.t = app.t + 1 + n := app.chain.getByIndex(app.t) + if n != nil { + f, ok := n.(Middleware) + if ok { + f(c) + } else { + ff, ok := n.(Handler) + if ok { + ff(c) + } + } + } +} + +type chain struct { + nodes []interface{} +} + +func (cn *chain) reset() { + cn.nodes = []interface{}{} +} + +func (c *chain) getByIndex(i int) interface{} { + for k := range c.nodes { + if k == i { + return c.nodes[i] + } + } + + return nil +} + +func (app *App) prepareChain(hs []interface{}) { + mw := app.middlewares.GetMiddlewares() + for _, v := range mw { + app.chain.nodes = append(app.chain.nodes, v) + } + for _, v := range hs { + app.chain.nodes = append(app.chain.nodes, v) + } +} + +func (cn *chain) execute(ctx *Context) { + i := cn.getByIndex(0) + if i != nil { + f, ok := i.(Middleware) + if ok { + f(ctx) + } else { + ff, ok := i.(Handler) + if ok { + ff(ctx) + } + } + } +} + +func (app *App) combHandlers(h Handler, mw []Middleware) []interface{} { + var rev []interface{} + for _, k := range mw { + rev = append(rev, k) + } + rev = append(rev, h) + return rev +} + +func getGormFunc() func() *gorm.DB { + f := func() *gorm.DB { + if !gormC.EnableGorm { + panic("you are trying to use gorm but it's not enabled, you can enable it in the file config/gorm.go") + } + return ResolveGorm() + } + return f +} + +func NewGorm() *gorm.DB { + var err error + switch os.Getenv("DB_DRIVER") { + case "mysql": + db, err = mysqlConnect() + case "postgres": + db, err = postgresConnect() + case "sqlite": + sqlitePath := os.Getenv("SQLITE_DB_PATH") + fullSqlitePath := path.Join(basePath, sqlitePath) + _, err := os.Stat(fullSqlitePath) + if err != nil { + panic(fmt.Sprintf("error locating sqlite file: %v", err.Error())) + } + db, err = gorm.Open(sqlite.Open(fullSqlitePath), &gorm.Config{}) + default: + panic("database driver not selected") + } + if gormC.EnableGorm && err != nil { + panic(fmt.Sprintf("gorm has problem connecting to %v, (if it's not needed you can disable it in config/gorm.go): %v", os.Getenv("DB_DRIVER"), err)) + } + return db +} + +func ResolveGorm() *gorm.DB { + if db != nil { + return db + } + db = NewGorm() + return db +} + +func resolveCache() func() *Cache { + f := func() *Cache { + if !cacheC.EnableCache { + panic("you are trying to use cache but it's not enabled, you can enable it in the file config/cache.go") + } + return NewCache(cacheC) + } + return f +} + +func postgresConnect() (*gorm.DB, error) { + dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=%v TimeZone=%v", + os.Getenv("POSTGRES_HOST"), + os.Getenv("POSTGRES_USER"), + os.Getenv("POSTGRES_PASSWORD"), + os.Getenv("POSTGRES_DB_NAME"), + os.Getenv("POSTGRES_PORT"), + os.Getenv("POSTGRES_SSL_MODE"), + os.Getenv("POSTGRES_TIMEZONE"), + ) + return gorm.Open(postgres.Open(dsn), &gorm.Config{}) +} + +func mysqlConnect() (*gorm.DB, error) { + dsn := fmt.Sprintf("%v:%v@tcp(%v:%v)/%v?charset=%v&parseTime=True&loc=Local", + os.Getenv("MYSQL_USERNAME"), + os.Getenv("MYSQL_PASSWORD"), + os.Getenv("MYSQL_HOST"), + os.Getenv("MYSQL_PORT"), + os.Getenv("MYSQL_DB_NAME"), + os.Getenv("MYSQL_CHARSET"), + ) + return gorm.Open(mysql.New(mysql.Config{ + DSN: dsn, // data source name + DefaultStringSize: 256, // default size for string fields + DisableDatetimePrecision: true, // disable datetime precision, which not supported before MySQL 5.6 + DontSupportRenameIndex: true, // drop & create when rename index, rename index not supported before MySQL 5.7, MariaDB + DontSupportRenameColumn: true, // `change` when rename column, rename column not supported before MySQL 8, MariaDB + SkipInitializeWithVersion: false, // auto configure based on currently MySQL version + }), &gorm.Config{}) +} + +func getJWT() func() *JWT { + f := func() *JWT { + secret := os.Getenv("JWT_SECRET") + if secret == "" { + panic("jwt secret key is not set") + } + lifetimeStr := os.Getenv("JWT_LIFESPAN_MINUTES") + if lifetimeStr == "" { + lifetimeStr = "10080" // 7 days + } + lifetime64, err := strconv.ParseInt(lifetimeStr, 10, 32) + if err != nil { + panic(err) + } + lifetime := int(lifetime64) + return newJWT(JWTOptions{ + SigningKey: secret, + LifetimeMinutes: lifetime, + }) + } + return f +} + +func getValidator() func() *Validator { + f := func() *Validator { + return &Validator{} + } + return f +} + +func resloveHashing() func() *Hashing { + f := func() *Hashing { + return &Hashing{} + } + return f +} + +func resolveMailer() func() *Mailer { + f := func() *Mailer { + if mailer != nil { + return mailer + } + var m *Mailer + var emailsDriver string + if os.Getenv("EMAILS_DRIVER") == "" { + emailsDriver = "SMTP" + } + switch emailsDriver { + case "SMTP": + m = initiateMailerWithSMTP() + case "sparkpost": + m = initiateMailerWithSparkPost() + case "sendgrid": + m = initiateMailerWithSendGrid() + case "mailgun": + return initiateMailerWithMailGun() + default: + m = initiateMailerWithSMTP() + } + mailer = m + return mailer + } + return f +} + +func resolveEventsManager() func() *EventsManager { + f := func() *EventsManager { + return ResolveEventsManager() + } + return f +} + +func resolveLogger() func() *logger.Logger { + f := func() *logger.Logger { + return loggr + } + return f +} + +func (app *App) MakeDirs(dirs ...string) { + o := syscall.Umask(0) + defer syscall.Umask(o) + for _, dir := range dirs { + os.MkdirAll(path.Join(basePath, dir), 0766) + } +} + +func (app *App) SetRequestConfig(r RequestConfig) { + requestC = r +} + +func (app *App) SetGormConfig(g GormConfig) { + gormC = g +} + +func (app *App) SetCacheConfig(c CacheConfig) { + cacheC = c +} + +func (app *App) SetBasePath(path string) { + basePath = path +} + +func DisableEvents() { + disableEvents = true +} + +func EnableEvents() { + disableEvents = false +} diff --git a/core_test.go b/core_test.go new file mode 100644 index 0000000..fc146af --- /dev/null +++ b/core_test.go @@ -0,0 +1,525 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "git.smarteching.com/goffee/core/env" + "git.smarteching.com/goffee/core/logger" + "github.com/google/uuid" + "github.com/joho/godotenv" + "github.com/julienschmidt/httprouter" +) + +func TestNew(t *testing.T) { + app := createNewApp(t) + if fmt.Sprintf("%T", app) != "*core.App" { + t.Errorf("failed testing new core") + } +} + +func TestSetEnv(t *testing.T) { + envVars, err := godotenv.Read("./testingdata/.env") + if err != nil { + t.Errorf("failed reading .env file") + } + + env.SetEnvVars(envVars) + + if os.Getenv("KEY_ONE") != "VAL_ONE" || os.Getenv("KEY_TWO") != "VAL_TWO" { + t.Errorf("failed to set env vars") + } +} + +func TestMakeHTTPHandlerFunc(t *testing.T) { + app := createNewApp(t) + tmpFile := filepath.Join(t.TempDir(), uuid.NewString()) + app.SetLogsDriver(&logger.LogFileDriver{ + FilePath: filepath.Join(t.TempDir(), uuid.NewString()), + }) + hdlr := Handler(func(c *Context) *Response { + f, _ := os.Create(tmpFile) + f.WriteString("DFT2V56H") + c.Response.SetHeader("header-key", "header-val") + return c.Response.Text("DFT2V56H") + }) + h := app.makeHTTPRouterHandlerFunc(hdlr, nil) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + h(w, r, []httprouter.Param{{Key: "tkey", Value: "tvalue"}}) + rsp := w.Result() + if rsp.StatusCode != http.StatusOK { + t.Errorf("failed testing make http handler func") + } + s, _ := os.ReadFile(tmpFile) + if string(s) != "DFT2V56H" { + t.Errorf("failed testing make http handler func") + } +} + +func TestMakeHTTPHandlerFuncVerifyJson(t *testing.T) { + app := createNewApp(t) + tmpFile := filepath.Join(t.TempDir(), uuid.NewString()) + app.SetLogsDriver(&logger.LogFileDriver{ + FilePath: filepath.Join(t.TempDir(), uuid.NewString()), + }) + hdlr := Handler(func(c *Context) *Response { + f, _ := os.Create(tmpFile) + f.WriteString("DFT2V56H") + c.Response.SetHeader("header-key", "header-val") + return c.Response.Json("{\"testKey\": \"testVal\"}") + }) + h := app.makeHTTPRouterHandlerFunc(hdlr, nil) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + h(w, r, []httprouter.Param{{Key: "tkey", Value: "tvalue"}}) + rsp := w.Result() + if rsp.StatusCode != http.StatusOK { + t.Errorf("failed testing make http handler func with json verify") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed testing make http handler func with json verify") + } + var j map[string]interface{} + err = json.Unmarshal(b, &j) + if err != nil { + t.Errorf("failed testing make http handler func with json verify: %v", err) + } + if j["testKey"] != "testVal" { + t.Errorf("failed testing make http handler func with json verify") + } +} + +func TestMethodNotAllowedHandler(t *testing.T) { + app := createNewApp(t) + app.SetLogsDriver(logger.LogNullDriver{}) + app.Bootstrap() + m := &methodNotAllowed{} + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + m.ServeHTTP(w, r) + rsp := w.Result() + if rsp.StatusCode != 405 { + t.Errorf("failed testing method not allowed") + } +} + +func TestNotFoundHandler(t *testing.T) { + n := ¬FoundHandler{} + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + n.ServeHTTP(w, r) + rsp := w.Result() + if rsp.StatusCode != 404 { + t.Errorf("failed testing not found handler") + } + +} + +func TestUseMiddleware(t *testing.T) { + app := createNewApp(t) + UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") })) + if len(app.middlewares.GetMiddlewares()) != 1 { + t.Errorf("failed testing use middleware") + } +} + +func TestChainReset(t *testing.T) { + c := &chain{} + c.nodes = append(c.nodes, Middleware(func(c *Context) { c.GetLogger().Info("Testing1!") })) + c.nodes = append(c.nodes, Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") })) + + c.reset() + if len(c.nodes) != 0 { + t.Errorf("failed testing reset chain") + } +} + +func TestNext(t *testing.T) { + app := createNewApp(t) + app.t = 0 + tfPath := filepath.Join(t.TempDir(), uuid.NewString()) + var hs []interface{} + hs = append(hs, Middleware(func(c *Context) { c.Next() })) + hs = append(hs, Handler(func(c *Context) *Response { + f, _ := os.Create(tfPath) + f.WriteString("DFT2V56H") + return nil + })) + app.prepareChain(hs) + + app.chain.execute(makeCTX(t)) + cnt, _ := os.ReadFile(tfPath) + if string(cnt) != "DFT2V56H" { + // t.Errorf("failed testing next") + } +} + +func TestChainGetByIndex(t *testing.T) { + c := &chain{} + tf := filepath.Join(t.TempDir(), uuid.NewString()) + var hs []interface{} + hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing!") })) + hs = append(hs, Middleware(func(c *Context) { + f, _ := os.Create(tf) + f.WriteString("DFT2V56H") + })) + c.nodes = hs + pf := c.getByIndex(1) + f, ok := pf.(Middleware) + if ok { + f(makeCTX(t)) + } + d, _ := os.ReadFile(tf) + if string(d) != "DFT2V56H" { + t.Errorf("failed testing chain get by index") + } +} + +func TestPrepareChain(t *testing.T) { + app := createNewApp(t) + UseMiddleware(Middleware(func(c *Context) { c.GetLogger().Info("Testing!") })) + var hs []interface{} + hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing1!") })) + hs = append(hs, Middleware(func(c *Context) { c.GetLogger().Info("testing2!") })) + app.prepareChain(hs) + if len(app.chain.nodes) != 3 { + t.Errorf("failed preparing chain") + } +} + +func TestChainExecute(t *testing.T) { + tmpDir := t.TempDir() + f1Path := filepath.Join(tmpDir, uuid.NewString()) + c := &chain{} + c.nodes = []interface{}{ + Handler(func(c *Context) *Response { + tf, _ := os.Create(f1Path) + defer tf.Close() + tf.WriteString("DFT2V56H") + return nil + }), + } + ctx := makeCTX(t) + c.execute(ctx) + cnt, _ := os.ReadFile(f1Path) + if string(cnt) != "DFT2V56H" { + t.Errorf("failed testing execute chain") + } +} + +func makeCTX(t *testing.T) *Context { + t.Helper() + return &Context{ + Request: &Request{ + httpRequest: httptest.NewRequest(GET, LOCALHOST, nil), + httpPathParams: nil, + }, + Response: &Response{ + headers: []header{}, + body: nil, + HttpResponseWriter: httptest.NewRecorder(), + }, + GetValidator: nil, + GetJWT: nil, + } +} + +func TestcombHndlers(t *testing.T) { + app := createNewApp(t) + t1 := Handler(func(c *Context) *Response { c.GetLogger().Info("Testing1!"); return nil }) + t2 := Middleware(func(c *Context) { c.GetLogger().Info("Testing2!") }) + + mw := []Middleware{t2} + comb := app.combHandlers(t1, mw) + if reflect.ValueOf(t1).Pointer() != reflect.ValueOf(comb[0]).Pointer() { + t.Errorf("failed testing reverse handlers") + } + + if reflect.ValueOf(t2).Pointer() != reflect.ValueOf(comb[1]).Pointer() { + t.Errorf("failed testing reverse handlers") + } +} + +func TestRegisterGetRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Get("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Post("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Delete("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Patch("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Put("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Options("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + gcr.Head("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("GET", s.URL+"?param=valget", nil) + if err != nil { + t.Errorf("failed test register routes") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register routes") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test register routes") + } + if strings.TrimSpace(string(b)) != "valget" { + t.Errorf("failed test register routes") + } + rsp.Body.Close() +} + +func TestRegisterPostRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Post("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("POST", s.URL+"?param=valpost", nil) + if err != nil { + t.Errorf("failed test register post route") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register post route") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test register post route") + } + if strings.TrimSpace(string(b)) != "valpost" { + t.Errorf("failed test register post route") + } + rsp.Body.Close() +} + +func TestRegisterDeleteRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Delete("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("DELETE", s.URL+"?param=valdelete", nil) + if err != nil { + t.Errorf("failed test register delete route") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register delete route") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test register delete route") + } + if strings.TrimSpace(string(b)) != "valdelete" { + t.Errorf("failed test register delete route") + } + rsp.Body.Close() +} + +func TestRegisterPatchRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Patch("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("PATCH", s.URL+"?param=valpatch", nil) + if err != nil { + t.Errorf("failed test register patch route") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register patch route") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test register patch route") + } + if strings.TrimSpace(string(b)) != "valpatch" { + t.Errorf("failed test register patch route") + } + rsp.Body.Close() +} + +func TestRegisterPutRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Put("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("PUT", s.URL+"?param=valput", nil) + if err != nil { + t.Errorf("failed test register put route") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register put route") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test register put route") + } + if strings.TrimSpace(string(b)) != "valput" { + t.Errorf("failed test register put route") + } + rsp.Body.Close() +} + +func TestRegisterOptionsRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + gcr.Options("/", Handler(func(c *Context) *Response { + fmt.Fprintln(c.Response.HttpResponseWriter, c.GetRequestParam("param")) + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("OPTIONS", s.URL+"?param=valoptions", nil) + if err != nil { + t.Errorf("failed test register options route") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register options route") + } + b, err := io.ReadAll(rsp.Body) + if err != nil { + t.Errorf("failed test register options route") + } + if strings.TrimSpace(string(b)) != "valoptions" { + t.Errorf("failed test register options route") + } + rsp.Body.Close() +} + +func TestRegisterHeadRoute(t *testing.T) { + app := New() + hr := httprouter.New() + gcr := NewRouter() + tfp := filepath.Join(t.TempDir(), uuid.NewString()) + gcr.Head("/", Handler(func(c *Context) *Response { + param := c.GetRequestParam("param") + p, _ := param.(string) + f, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR, 777) + if err != nil { + fmt.Println(err.Error()) + } + defer f.Close() + f.WriteString("fromhead") + return nil + })) + hr = app.RegisterRoutes(gcr.GetRoutes(), hr) + s := httptest.NewServer(hr) + defer s.Close() + clt := &http.Client{} + req, err := http.NewRequest("HEAD", s.URL+"?param="+tfp, nil) + if err != nil { + t.Errorf("failed test register head route") + } + rsp, err := clt.Do(req) + if err != nil { + t.Errorf("failed test register head route") + } + f, err := os.Open(tfp) + if err != nil { + t.Errorf("failed test register head route: %v", err.Error()) + } + b, err := io.ReadAll(f) + if err != nil { + t.Errorf("failed test register head route: %v", err.Error()) + } + f.Close() + if strings.TrimSpace(string(b)) != "fromhead" { + t.Errorf("failed test register head route") + } + rsp.Body.Close() +} + +func TestPanicHandler(t *testing.T) { + os.Setenv("APP_DEBUG_MODE", "true") + loggr = logger.NewLogger(&logger.LogNullDriver{}) + r := httptest.NewRequest(GET, LOCALHOST, nil) + w := httptest.NewRecorder() + panicHandler(w, r, "") + rsp := w.Result() + b, _ := io.ReadAll(rsp.Body) + if !strings.Contains(string(b), "stack trace") { + t.Errorf("failed test panic handler") + } +} + +func createNewApp(t *testing.T) *App { + t.Helper() + a := New() + a.SetLogsDriver(&logger.LogNullDriver{}) + a.SetRequestConfig(testingRequestC) + + return a +} diff --git a/env/env.go b/env/env.go new file mode 100644 index 0000000..62cc10e --- /dev/null +++ b/env/env.go @@ -0,0 +1,32 @@ +package env + +import ( + "os" + "strings" +) + +func GetVar(varName string) string { + return os.Getenv(varName) +} + +func GetVarOtherwiseDefault(varName string, defaultValue string) string { + v, p := os.LookupEnv(varName) + if p { + return v + } + return defaultValue +} + +func IsSet(varName string) bool { + _, p := os.LookupEnv(varName) + if p { + return true + } + return false +} + +func SetEnvVars(envVars map[string]string) { + for key, val := range envVars { + os.Setenv(strings.TrimSpace(key), strings.TrimSpace(val)) + } +} diff --git a/env/env_test.go b/env/env_test.go new file mode 100644 index 0000000..f856dc3 --- /dev/null +++ b/env/env_test.go @@ -0,0 +1,53 @@ +package env + +import ( + "os" + "testing" +) + +func TestGetVar(t *testing.T) { + os.Setenv("testKey11", "testVal") + v := GetVar("testKey11") + if v != "testVal" { + t.Error("failed testing get var") + } +} + +func TestGetVarOtherwiseDefault(t *testing.T) { + v := GetVarOtherwiseDefault("testKey12", "defaultVal") + if v != "defaultVal" { + t.Error("failed testing get default") + } + os.Setenv("testKey12", "testVal") + v = GetVarOtherwiseDefault("testKey12", "defaultVal") + if v != "testVal" { + t.Error("failed testing get default val") + } +} + +func TestIsSet(t *testing.T) { + i := IsSet("testKey13") + if i == true { + t.Error("failed testing is set") + } + os.Setenv("testKey13", "testVal") + i = IsSet("testKey13") + if i == false { + t.Error("filed testing is set") + } +} + +func TestSetEnvVars(t *testing.T) { + envVars := map[string]string{ + "key14": "testVal14", + "key15": "testVal15", + } + + SetEnvVars(envVars) + if GetVar("key14") != "testVal14" { + t.Error("failed testing set vars") + } + if GetVar("key15") != "testVal15" { + t.Error("failed testing set vars") + } +} diff --git a/event-job.go b/event-job.go new file mode 100644 index 0000000..05605c9 --- /dev/null +++ b/event-job.go @@ -0,0 +1,3 @@ +package core + +type EventJob func(event *Event, requestContext *Context) diff --git a/event.go b/event.go new file mode 100644 index 0000000..8d41de3 --- /dev/null +++ b/event.go @@ -0,0 +1,6 @@ +package core + +type Event struct { + Name string + Payload map[string]interface{} +} diff --git a/events-manager.go b/events-manager.go new file mode 100644 index 0000000..f71dd79 --- /dev/null +++ b/events-manager.go @@ -0,0 +1,89 @@ +package core + +import ( + "errors" + "fmt" +) + +type EventsManager struct { + eventsJobsList map[string][]EventJob + firedEvents []*Event +} + +var manager *EventsManager +var rqc *Context + +func NewEventsManager() *EventsManager { + manager = &EventsManager{ + eventsJobsList: map[string][]EventJob{}, + } + + return manager +} + +func ResolveEventsManager() *EventsManager { + return manager +} + +func (m *EventsManager) setContext(requestContext *Context) *EventsManager { + rqc = requestContext + + return m +} + +func (m *EventsManager) Fire(e *Event) error { + if disableEvents { + return nil + } + if e.Name == "" { + return errors.New("event name is empty") + } + _, exists := m.eventsJobsList[e.Name] + if !exists { + return errors.New(fmt.Sprintf("event %v is not registered", e.Name)) + } + + m.firedEvents = append(m.firedEvents, e) + return nil +} + +func (m *EventsManager) Register(eName string, job EventJob) { + if disableEvents { + return + } + if eName == "" { + panic("event name is empty") + } + _, exists := m.eventsJobsList[eName] + if !exists { + m.eventsJobsList[eName] = []EventJob{job} + return + } + + for key, jobs := range m.eventsJobsList { + if key == eName { + jobs = append(jobs, job) + m.eventsJobsList[key] = jobs + } + } +} + +func (m *EventsManager) processFiredEvents() { + if disableEvents { + return + } + for _, event := range m.firedEvents { + m.executeEventJobs(event) + } + m.firedEvents = []*Event{} +} + +func (m *EventsManager) executeEventJobs(event *Event) { + for key, jobs := range m.eventsJobsList { + if key == event.Name { + for _, job := range jobs { + job(event, rqc) + } + } + } +} diff --git a/events-manager_test.go b/events-manager_test.go new file mode 100644 index 0000000..4630b21 --- /dev/null +++ b/events-manager_test.go @@ -0,0 +1,103 @@ +package core + +import ( + "fmt" + "testing" +) + +func TestNewEventsManager(t *testing.T) { + m := NewEventsManager() + if fmt.Sprintf("%T", m) != "*core.EventsManager" { + t.Errorf("failed testing new events manager") + } +} + +// func TestResolveEventsManager(t *testing.T) { +// NewEventsManager() +// m := ResolveEventsManager() +// if fmt.Sprintf("%T", m) != "*core.EventsManager" { +// t.Errorf("failed testing new events manager") +// } +// } + +// func TestEvents(t *testing.T) { +// pwd, _ := os.Getwd() +// const eventName1 string = "test-event-name1" +// const eventName2 string = "test-event-name2" +// var tmpDir string +// if runtime.GOOS == "linux" { +// tmpDir = t.TempDir() +// } else { +// tmpDir = filepath.Join(pwd, "/testingdata/tmp") +// } +// tmpFile1 := filepath.Join(tmpDir, uuid.NewString()) +// tmpFile2 := filepath.Join(tmpDir, uuid.NewString()) +// tmpFile3 := filepath.Join(tmpDir, uuid.NewString()) +// m := NewEventsManager() +// m.Register(eventName1, func(event *Event, requestContext *Context) { +// os.Create(tmpFile1) +// f, err := os.Create(tmpFile1) +// if err != nil { +// t.Errorf("error testing register event: %v", err.Error()) +// } +// f.WriteString(event.Name) +// f.Close() +// }) +// m.Register(eventName1, func(event *Event, requestContext *Context) { +// os.Create(tmpFile3) +// f, err := os.Create(tmpFile3) +// if err != nil { +// t.Errorf("error testing register event: %v", err.Error()) +// } +// f.WriteString(event.Name) +// f.Close() +// }) +// m.Fire(&Event{Name: eventName1}) +// m.processFiredEvents() + +// ff, err := os.Open(tmpFile1) +// if err != nil { +// t.Errorf("error testing register event : %v", err.Error()) +// } + +// d, err := io.ReadAll(ff) +// if string(d) != eventName1 { +// t.Error("faild testing events") +// } +// ff.Close() +// os.Remove(tmpFile1) + +// ff, err = os.Open(tmpFile3) +// if err != nil { +// t.Errorf("error testing register event : %v", err.Error()) +// } + +// d, err = io.ReadAll(ff) +// if string(d) != eventName1 { +// t.Error("faild testing events") +// } +// ff.Close() +// os.Remove(tmpFile3) + +// m.Register(eventName2, func(event *Event, requestContext *Context) { +// f, err := os.Create(tmpFile2) +// if err != nil { +// t.Errorf("error testing register event: %v", err.Error()) +// } +// f.WriteString(event.Name) +// f.Close() +// }) +// m.Fire(&Event{Name: eventName2}) +// m.processFiredEvents() + +// ff, err = os.Open(tmpFile2) +// if err != nil { +// t.Errorf("error testing register event : %v", err.Error()) +// } + +// d, err = io.ReadAll(ff) +// if string(d) != eventName2 { +// t.Error("faild testing events") +// } +// ff.Close() +// } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..a3e044e --- /dev/null +++ b/go.mod @@ -0,0 +1,50 @@ +module git.smarteching.com/goffee/core + +replace git.smarteching.com/goffee/core/logger => ./logger + +replace git.smarteching.com/goffee/core/env => ./env + +go 1.20 + +require ( + github.com/brianvoe/gofakeit/v6 v6.21.0 + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.3.0 + github.com/harranali/mailing v1.2.0 + github.com/joho/godotenv v1.5.1 + github.com/julienschmidt/httprouter v1.3.0 + github.com/redis/go-redis/v9 v9.0.5 + golang.org/x/crypto v0.11.0 + gorm.io/driver/mysql v1.5.1 + gorm.io/driver/postgres v1.5.2 + gorm.io/driver/sqlite v1.5.2 + gorm.io/gorm v1.25.2 +) + +require ( + github.com/SparkPost/gosparkpost v0.2.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/go-chi/chi/v5 v5.0.8 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.3.1 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/mailgun/mailgun-go/v4 v4.10.0 // indirect + github.com/mattn/go-sqlite3 v1.14.17 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sendgrid/rest v2.6.9+incompatible // indirect + github.com/sendgrid/sendgrid-go v3.12.0+incompatible // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/text v0.11.0 // indirect +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-ozzo/ozzo-validation v3.6.0+incompatible + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4aedeaa --- /dev/null +++ b/go.sum @@ -0,0 +1,109 @@ +github.com/SparkPost/gosparkpost v0.2.0 h1:yzhHQT7cE+rqzd5tANNC74j+2x3lrPznqPJrxC1yR8s= +github.com/SparkPost/gosparkpost v0.2.0/go.mod h1:S9WKcGeou7cbPpx0kTIgo8Q69WZvUmVeVzbD+djalJ4= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/brianvoe/gofakeit/v6 v6.21.0 h1:tNkm9yxEbpuPK8Bx39tT4sSc5i9SUGiciLdNix+VDQY= +github.com/brianvoe/gofakeit/v6 v6.21.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8= +github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/buger/jsonparser v1.0.0/go.mod h1:tgcrVJ81GPSF0mz+0nu1Xaz0fazGPrmmJfJtxjbHhUQ= +github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= +github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y= +github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible h1:msy24VGS42fKO9K1vLz82/GeYW1cILu7Nuuj1N3BBkE= +github.com/go-ozzo/ozzo-validation v3.6.0+incompatible/go.mod h1:gsEKFIVnabGBt6mXmxK0MoFy+cZoTJY6mu5Ll3LVLBU= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/harranali/mailing v1.2.0 h1:ihIyJwB8hyRVcdk+v465wk1PHMrSrgJqo/kMd+gZClY= +github.com/harranali/mailing v1.2.0/go.mod h1:4a5N3yG98pZKluMpmcYlTtll7bisvOfGQEMIng3VQk4= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= +github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= +github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= +github.com/jhillyerd/enmime v0.8.0/go.mod h1:MBHs3ugk03NGjMM6PuRynlKf+HA5eSillZ+TRCm73AE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailgun/mailgun-go/v4 v4.10.0 h1:e5LVsxpqjOYRyaOWifrJORoLQZTYDP+g4ljfmf9G2zE= +github.com/mailgun/mailgun-go/v4 v4.10.0/go.mod h1:L9s941Lgk7iB3TgywTPz074pK2Ekkg4kgbnAaAyJ2z8= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0= +github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE= +github.com/sendgrid/sendgrid-go v3.12.0+incompatible h1:/N2vx18Fg1KmQOh6zESc5FJB8pYwt5QFBDflYPh1KVg= +github.com/sendgrid/sendgrid-go v3.12.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw= +gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o= +gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= +gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= +gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc= +gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= +gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..c3ec69f --- /dev/null +++ b/handler.go @@ -0,0 +1,8 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +type Handler func(c *Context) *Response diff --git a/hashing.go b/hashing.go new file mode 100644 index 0000000..87db07d --- /dev/null +++ b/hashing.go @@ -0,0 +1,37 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "errors" + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +type Hashing struct{} + +func (h *Hashing) HashPassword(password string) (string, error) { + res, err := bcrypt.GenerateFromPassword([]byte(password), 10) + if err != nil { + return "", err + } + return string(res), nil +} + +func (h *Hashing) CheckPasswordHash(hashedPassword string, originalPassowrd string) (bool, error) { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(originalPassowrd)) + if err != nil && errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return false, nil + } + + if err != nil { + fmt.Print(err) + loggr.Debug(err.Error()) + return false, errors.New("failed checking password hash") + } + return true, nil +} diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..7768d09 --- /dev/null +++ b/jwt.go @@ -0,0 +1,100 @@ +package core + +import ( + "encoding/json" + "errors" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +type JWT struct { + signingKey []byte + expiresAt time.Time +} +type JWTOptions struct { + SigningKey string + LifetimeMinutes int +} + +var j *JWT + +func newJWT(opts JWTOptions) *JWT { + d := time.Duration(opts.LifetimeMinutes) + j = &JWT{ + signingKey: []byte(opts.SigningKey), + expiresAt: time.Now().Add(d * time.Minute), + } + return j +} +func resolveJWT() *JWT { + return j +} + +type claims struct { + J []byte + jwt.RegisteredClaims +} + +func (j *JWT) GenerateToken(payload map[string]interface{}) (string, error) { + claims, err := mapClaims(payload, j.expiresAt) + if err != nil { + return "", err + } + t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := t.SignedString(j.signingKey) + if err != nil { + return "", err + } + return token, nil +} + +func (j *JWT) DecodeToken(token string) (payload map[string]interface{}, err error) { + t, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) { + return j.signingKey, nil + }) + if err != nil { + return nil, err + } + c, ok := t.Claims.(*claims) + if !ok { + return nil, errors.New("error decoding token") + } + expiresAt := time.Unix(c.ExpiresAt.Unix(), 0) + et := time.Now().Compare(expiresAt) + if et != -1 { + return nil, errors.New("token has expired") + } + err = json.Unmarshal(c.J, &payload) + if err != nil { + return nil, err + } + return payload, nil +} + +func (j *JWT) HasExpired(token string) (bool, error) { + _, err := jwt.ParseWithClaims(token, &claims{}, func(token *jwt.Token) (interface{}, error) { + return j.signingKey, nil + }) + if err != nil { + if errors.Is(err, jwt.ErrTokenExpired) { + return true, nil + } + return true, err + } + return false, nil +} + +func mapClaims(data map[string]interface{}, expiresAt time.Time) (jwt.Claims, error) { + j, err := json.Marshal(data) + if err != nil { + return claims{}, err + } + r := claims{ + j, + jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(expiresAt), + }, + } + return r, nil +} diff --git a/jwt_test.go b/jwt_test.go new file mode 100644 index 0000000..c7bf7a8 --- /dev/null +++ b/jwt_test.go @@ -0,0 +1,114 @@ +package core + +import ( + "fmt" + "testing" + "time" +) + +func TestNewJWT(t *testing.T) { + j := newJWT(JWTOptions{ + SigningKey: "testsigning", + LifetimeMinutes: 2, + }) + + if fmt.Sprintf("%T", j) != "*core.JWT" { + t.Errorf("failed testing new jwt") + } +} + +func TestResolveJWT(t *testing.T) { + initiateJWTHelper(t) + j := resolveJWT() + if fmt.Sprintf("%T", j) != "*core.JWT" { + t.Errorf("failed testing resolve jwt") + } +} + +func TestGenerateToken(t *testing.T) { + j := initiateJWTHelper(t) + token, err := j.GenerateToken(map[string]interface{}{ + "testKey": "testVal", + }) + if err != nil || token == "" { + t.Errorf("error testing generate jwt token") + } + d, err := j.DecodeToken(token) + if err != nil { + t.Errorf("error testing generate jwt token: %v", err.Error()) + } + if d["testKey"] != "testVal" { + t.Errorf("error testing generate jwt token: %v", err.Error()) + } +} + +func TestDecodeToken(t *testing.T) { + expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJKIjoiZXlKMFpYTjBTMlY1SWpvaWRHVnpkRlpoYkNKOSIsImV4cCI6MTY4NDkyMzQwOX0.v2aM9OTDJ48L4KnGjfLH3JAFQw4Gkgj5z7cA7txPNag" + j := initiateJWTHelper(t) + _, err := j.DecodeToken(expiredToken) + if err == nil { + t.Errorf("failed test decode token") + } + token, err := j.GenerateToken(map[string]interface{}{ + "testKey": "testVal", + }) + if err != nil || token == "" { + t.Errorf("failed testing decode token") + } + d, err := j.DecodeToken(token) + if err != nil { + t.Errorf("error testing decode jwt token") + } + if d["testKey"] != "testVal" { + t.Errorf("error testing decode jwt token") + } + + d, err = j.DecodeToken("test-token") + if err == nil { + t.Errorf("error testing decode jwt token") + } +} + +func TestHasExpired(t *testing.T) { + expiredToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJKIjoiZXlKMFpYTjBTMlY1SWpvaWRHVnpkRlpoYkNKOSIsImV4cCI6MTY4NDkyMzQwOX0.v2aM9OTDJ48L4KnGjfLH3JAFQw4Gkgj5z7cA7txPNag" + j := initiateJWTHelper(t) + tokenHasExpired, err := j.HasExpired(expiredToken) + if !tokenHasExpired { + t.Errorf("failed test token has expired check") + } + if tokenHasExpired && err != nil { + t.Errorf("failed test decode token: %v", err.Error()) + } + token, err := j.GenerateToken(map[string]interface{}{ + "testKey": "testVal", + }) + if err != nil || token == "" { + t.Errorf("failed testing decode token") + } + _, err = j.HasExpired(token) + if err != nil { + t.Errorf("error testing decode jwt token") + } +} + +func TestMapClaims(t *testing.T) { + c, err := mapClaims(map[string]interface{}{ + "testKey": "testVal", + }, time.Now()) + + if err != nil { + t.Errorf("failed testing map claims") + } + if fmt.Sprintf("%T", c) != "core.claims" { + t.Errorf("failed testing map claims") + } +} + +func initiateJWTHelper(t *testing.T) *JWT { + t.Helper() + j := newJWT(JWTOptions{ + SigningKey: "testsigning", + LifetimeMinutes: 2, + }) + return j +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..4746c9f --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,94 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package logger + +import ( + "io" + "log" + "os" +) + +// logs file +var logsFile *os.File + +type Logger struct { + infoLogger *log.Logger + warningLogger *log.Logger + errorLogger *log.Logger + debugLogger *log.Logger +} + +var l *Logger + +type LogsDriver interface { + GetTarget() interface{} +} + +type LogFileDriver struct { + FilePath string +} + +type LogNullDriver struct{} + +func (n LogNullDriver) GetTarget() interface{} { + return nil +} + +func (f LogFileDriver) GetTarget() interface{} { + return f.FilePath +} + +func NewLogger(driver LogsDriver) *Logger { + if driver.GetTarget() == nil { + l = &Logger{ + infoLogger: log.New(io.Discard, "info: ", log.LstdFlags), + warningLogger: log.New(io.Discard, "warning: ", log.LstdFlags), + errorLogger: log.New(io.Discard, "error: ", log.LstdFlags), + debugLogger: log.New(io.Discard, "debug: ", log.LstdFlags), + } + return l + } + path, ok := driver.GetTarget().(string) + if !ok { + panic("something wrong with the file path") + } + logsFile, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) + if err != nil { + panic(err) + } + l = &Logger{ + infoLogger: log.New(logsFile, "info: ", log.LstdFlags), + warningLogger: log.New(logsFile, "warning: ", log.LstdFlags), + errorLogger: log.New(logsFile, "error: ", log.LstdFlags), + debugLogger: log.New(logsFile, "debug: ", log.LstdFlags), + } + return l +} + +func ResolveLogger() *Logger { + return l +} + +func (l *Logger) Info(msg interface{}) { + l.infoLogger.Println(msg) +} + +func (l *Logger) Debug(msg interface{}) { + l.debugLogger.Println(msg) +} + +func (l *Logger) Warning(msg interface{}) { + l.warningLogger.Println(msg) +} + +func (l *Logger) Error(msg interface{}) { + l.errorLogger.Println(msg) +} + +func CloseLogsFile() { + if logsFile != nil { + defer logsFile.Close() + } +} diff --git a/logger/logger_test.go b/logger/logger_test.go new file mode 100644 index 0000000..f82e8a1 --- /dev/null +++ b/logger/logger_test.go @@ -0,0 +1,133 @@ +package logger + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/uuid" +) + +func TestNewLogger(t *testing.T) { + fp := filepath.Join(t.TempDir(), uuid.NewString()) + f, err := os.Create(fp) + if err != nil { + t.Errorf("failed test new logger") + } + f.Close() + l := NewLogger(&LogNullDriver{}) + l.Info("testing") + fdrv := &LogFileDriver{ + fp, + } + trgt := fdrv.GetTarget() + ts, ok := trgt.(string) + if !ok { + t.Errorf("failed test new logger") + } + if ts != fp { + t.Errorf("failed test new logger") + } + l = NewLogger(fdrv) + l.Error("test-err") + f, err = os.Open(fp) + if err != nil { + t.Errorf("failed test new logger") + } + defer f.Close() + b, err := io.ReadAll(f) + if !strings.Contains(string(b), "test-err") { + t.Errorf("failed test new logger") + } +} + +func TestInfo(t *testing.T) { + path := filepath.Join(t.TempDir(), uuid.NewString()) + l := NewLogger(&LogFileDriver{ + FilePath: path, + }) + l.Info("DFT2V56H") + lf, err := os.Open(path) + if err != nil { + t.Error("failed testing info") + } + d, err := io.ReadAll(lf) + if err != nil { + t.Error("error testing info") + } + if !strings.Contains(string(d), "DFT2V56H") { + t.Error("error testing info") + } + t.Cleanup(func() { + CloseLogsFile() + }) +} + +func TestWarning(t *testing.T) { + path := filepath.Join(t.TempDir(), uuid.NewString()) + l := NewLogger(&LogFileDriver{ + FilePath: path, + }) + + l.Warning("DFT2V56H") + lf, err := os.Open(path) + if err != nil { + t.Error("failed testing warning") + } + d, err := io.ReadAll(lf) + if err != nil { + t.Error("failed testing warning") + } + if !strings.Contains(string(d), "DFT2V56H") { + t.Error("failed testing warning") + } + t.Cleanup(func() { + CloseLogsFile() + }) +} + +func TestDebug(t *testing.T) { + path := filepath.Join(t.TempDir(), uuid.NewString()) + l := NewLogger(&LogFileDriver{ + FilePath: path, + }) + l.Debug("DFT2V56H") + lf, err := os.Open(path) + if err != nil { + t.Error("failed testing debug") + } + d, err := io.ReadAll(lf) + if err != nil { + t.Error("error testing debug") + } + if !strings.Contains(string(d), "DFT2V56H") { + t.Error("error testing debug") + } + t.Cleanup(func() { + CloseLogsFile() + }) +} + +func TestError(t *testing.T) { + path := filepath.Join(t.TempDir(), uuid.NewString()) + l := NewLogger(&LogFileDriver{ + FilePath: path, + }) + l.Error("DFT2V56H") + lf, err := os.Open(path) + if err != nil { + t.Error("failed testing error") + } + d, err := io.ReadAll(lf) + if err != nil { + t.Error("failed testing error") + } + if !strings.Contains(string(d), "DFT2V56H") { + t.Error("failed testing error") + } + t.Cleanup(func() { + CloseLogsFile() + }) +} diff --git a/mailer.go b/mailer.go new file mode 100644 index 0000000..3e03050 --- /dev/null +++ b/mailer.go @@ -0,0 +1,183 @@ +// Copyright 2023 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "crypto/tls" + "fmt" + "net/mail" + "os" + "strconv" + + "github.com/harranali/mailing" +) + +type Mailer struct { + mailer *mailing.Mailer + sender mail.Address + receiver mail.Address + cc []mail.Address + bcc []mail.Address + subject string + htmlBody string + plainText string + attachment string +} + +type EmailAddress struct { + Name string // the name can be empty + Address string // ex: john@example.com +} +type EmailAttachment struct { + Name string // name of the file + Path string // full path to the file +} + +func initiateMailerWithSMTP() *Mailer { + portStr := os.Getenv("SMTP_PORT") + if portStr == "" { + panic("error reading smtp port env var") + } + port, err := strconv.ParseInt(portStr, 10, 64) + if err != nil { + panic(fmt.Sprintf("error parsing smtp port env var: %v", err)) + } + skipTlsVerifyStr := os.Getenv("SMTP_TLS_SKIP_VERIFY_HOST") + if skipTlsVerifyStr == "" { + panic("error reading smtp tls verify env var") + } + skipTlsVerify, err := strconv.ParseBool(skipTlsVerifyStr) + if err != nil { + panic(fmt.Sprintf("error parsing smtp tls verify env var: %v", err)) + } + + return &Mailer{ + mailer: mailing.NewMailerWithSMTP(&mailing.SMTPConfig{ + Host: os.Getenv("SMTP_HOST"), + Port: int(port), + Username: os.Getenv("SMTP_USERNAME"), + Password: os.Getenv("SMTP_PASSWORD"), + TLSConfig: tls.Config{ + ServerName: os.Getenv("SMTP_HOST"), + InsecureSkipVerify: skipTlsVerify, + }, + }), + } + +} + +func initiateMailerWithSparkPost() *Mailer { + apiVersionStr := os.Getenv("SPARKPOST_API_VERSION") + if apiVersionStr == "" { + panic("error reading sparkpost base url env var") + } + apiVersion, err := strconv.ParseInt(apiVersionStr, 10, 64) + if err != nil { + panic(fmt.Sprintf("error parsing sparkpost base url env var: %v", apiVersion)) + } + return &Mailer{ + mailer: mailing.NewMailerWithSparkPost(&mailing.SparkPostConfig{ + BaseUrl: os.Getenv("SPARKPOST_BASE_URL"), + ApiKey: os.Getenv("SPARKPOST_API_KEY"), + ApiVersion: int(apiVersion), + }), + } +} + +func initiateMailerWithSendGrid() *Mailer { + return &Mailer{ + mailer: mailing.NewMailerWithSendGrid(&mailing.SendGridConfig{ + Host: os.Getenv("SENDGRID_HOST"), + Endpoint: os.Getenv("SENDGRID_ENDPOINT"), + ApiKey: os.Getenv("SENDGRID_API_KEY"), + }), + } +} + +func initiateMailerWithMailGun() *Mailer { + skipTlsVerifyStr := os.Getenv("MAILGUN_TLS_SKIP_VERIFY_HOST") + if skipTlsVerifyStr == "" { + panic("error reading mailgun tls verify env var") + } + skipTlsVerify, err := strconv.ParseBool(skipTlsVerifyStr) + if err != nil { + panic(fmt.Sprintf("error parsing mailgun tls verify env var: %v", err)) + } + return &Mailer{ + mailer: mailing.NewMailerWithMailGun(&mailing.MailGunConfig{ + Domain: os.Getenv("MAILGUN_DOMAIN"), + APIKey: os.Getenv("MAILGUN_API_KEY"), + SkipTLSVerification: skipTlsVerify, + }), + } +} + +func (m *Mailer) SetFrom(emailAddresses EmailAddress) *Mailer { + e := mailing.EmailAddress{ + Name: emailAddresses.Name, + Address: emailAddresses.Address, + } + m.mailer.SetFrom(e) + return m +} + +func (m *Mailer) SetTo(emailAddresses []EmailAddress) *Mailer { + var addressesList []mailing.EmailAddress + for _, v := range emailAddresses { + addressesList = append(addressesList, mailing.EmailAddress{Name: v.Name, Address: v.Address}) + } + + m.mailer.SetTo(addressesList) + return m +} + +func (m *Mailer) SetCC(emailAddresses []EmailAddress) *Mailer { + var addressesList []mailing.EmailAddress + for _, v := range emailAddresses { + addressesList = append(addressesList, mailing.EmailAddress{Name: v.Name, Address: v.Address}) + } + + m.mailer.SetCC(addressesList) + return m +} + +func (m *Mailer) SetBCC(emailAddresses []EmailAddress) *Mailer { + var addressesList []mailing.EmailAddress + for _, v := range emailAddresses { + addressesList = append(addressesList, mailing.EmailAddress{Name: v.Name, Address: v.Address}) + } + + m.mailer.SetBCC(addressesList) + return m +} + +func (m *Mailer) SetSubject(subject string) *Mailer { + m.mailer.SetSubject(subject) + return m +} + +func (m *Mailer) SetHTMLBody(body string) *Mailer { + m.mailer.SetHTMLBody(body) + return m +} + +func (m *Mailer) SetPlainTextBody(body string) *Mailer { + m.mailer.SetPlainTextBody(body) + return m +} + +func (m *Mailer) SetAttachments(attachments []EmailAttachment) *Mailer { + var aList []mailing.Attachment + for _, v := range attachments { + aList = append(aList, mailing.Attachment{Name: v.Name, Path: v.Path}) + } + m.mailer.SetAttachments(aList) + return m +} + +func (m *Mailer) Send() error { + return m.mailer.Send() +} diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..2ff567c --- /dev/null +++ b/middleware.go @@ -0,0 +1,8 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +type Middleware func(c *Context) diff --git a/middlewares.go b/middlewares.go new file mode 100644 index 0000000..1d009fb --- /dev/null +++ b/middlewares.go @@ -0,0 +1,40 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +type Middlewares struct { + middlewares []Middleware +} + +var m *Middlewares + +func NewMiddlewares() *Middlewares { + m = &Middlewares{} + return m +} + +func ResolveMiddlewares() *Middlewares { + return m +} + +func (m *Middlewares) Attach(mw Middleware) *Middlewares { + m.middlewares = append(m.middlewares, mw) + + return m +} + +func (m *Middlewares) GetMiddlewares() []Middleware { + return m.middlewares +} + +func (m *Middlewares) getByIndex(i int) Middleware { + for k := range m.middlewares { + if k == i { + return m.middlewares[i] + } + } + return nil +} diff --git a/middlewares_test.go b/middlewares_test.go new file mode 100644 index 0000000..ef43264 --- /dev/null +++ b/middlewares_test.go @@ -0,0 +1,65 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "fmt" + "reflect" + "testing" +) + +func TestNewMiddlewares(t *testing.T) { + mw := NewMiddlewares() + if fmt.Sprintf("%T", mw) != "*core.Middlewares" { + t.Errorf("failed testing new middleware") + } +} + +func TestResloveMiddleWares(t *testing.T) { + NewMiddlewares() + mw := ResolveMiddlewares() + if fmt.Sprintf("%T", mw) != "*core.Middlewares" { + t.Errorf("failed resolve middlewares") + } +} + +func TestAttach(t *testing.T) { + mw := NewMiddlewares() + tmw := Middleware(func(c *Context) { + c.GetLogger().Info("Testing!") + }) + mw.Attach(tmw) + mws := mw.getByIndex(0) + if reflect.ValueOf(tmw).Pointer() != reflect.ValueOf(mws).Pointer() { + t.Errorf("Failed testing attach middleware") + } +} + +func TestGetMiddleWares(t *testing.T) { + mw := NewMiddlewares() + t1 := Middleware(func(c *Context) { + c.GetLogger().Info("testing1!") + }) + t2 := Middleware(func(c *Context) { + c.GetLogger().Info("testing2!") + }) + mw.Attach(t1) + mw.Attach(t2) + if len(mw.GetMiddlewares()) != 2 { + t.Errorf("failed testing get middlewares") + } +} + +func TestMiddlewareGetByIndex(t *testing.T) { + mw := NewMiddlewares() + t1 := Middleware(func(c *Context) { + c.GetLogger().Info("testing!") + }) + mw.Attach(t1) + if reflect.ValueOf(mw.getByIndex(0)).Pointer() != reflect.ValueOf(t1).Pointer() { + t.Errorf("failed testing get by index") + } +} diff --git a/request.go b/request.go new file mode 100644 index 0000000..a93ac62 --- /dev/null +++ b/request.go @@ -0,0 +1,17 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "net/http" + + "github.com/julienschmidt/httprouter" +) + +type Request struct { + httpRequest *http.Request + httpPathParams httprouter.Params +} diff --git a/response.go b/response.go new file mode 100644 index 0000000..1b869df --- /dev/null +++ b/response.go @@ -0,0 +1,183 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "fmt" + "net/http" +) + +type Response struct { + headers []header + body []byte + statusCode int + contentType string + overrideContentType string + isTerminated bool + redirectTo string + HttpResponseWriter http.ResponseWriter +} + +type header struct { + key string + val string +} + +// TODO add doc +func (rs *Response) Any(body any) *Response { + if rs.isTerminated == false { + rs.contentType = CONTENT_TYPE_HTML + rs.body = []byte(rs.castBasicVarsToString(body)) + } + + return rs +} + +// TODO add doc +func (rs *Response) Json(body string) *Response { + if rs.isTerminated == false { + rs.contentType = CONTENT_TYPE_JSON + rs.body = []byte(body) + } + return rs +} + +// TODO add doc +func (rs *Response) Text(body string) *Response { + if rs.isTerminated == false { + rs.contentType = CONTENT_TYPE_TEXT + rs.body = []byte(body) + } + return rs +} + +// TODO add doc +func (rs *Response) HTML(body string) *Response { + if rs.isTerminated == false { + rs.contentType = CONTENT_TYPE_HTML + rs.body = []byte(body) + } + return rs +} + +// TODO add doc +func (rs *Response) SetStatusCode(code int) *Response { + if rs.isTerminated == false { + rs.statusCode = code + } + + return rs +} + +// TODO add doc +func (rs *Response) SetContentType(c string) *Response { + if rs.isTerminated == false { + rs.overrideContentType = c + } + + return rs +} + +// TODO add doc +func (rs *Response) SetHeader(key string, val string) *Response { + if rs.isTerminated == false { + h := header{ + key: key, + val: val, + } + rs.headers = append(rs.headers, h) + } + return rs +} + +func (rs *Response) ForceSendResponse() { + rs.isTerminated = true +} + +func (rs *Response) Redirect(url string) *Response { + validator := resolveValidator() + v := validator.Validate(map[string]interface{}{ + "url": url, + }, map[string]interface{}{ + "url": "url", + }) + if v.Failed() { + if url[0:1] != "/" { + rs.redirectTo = "/" + url + } else { + rs.redirectTo = url + } + return rs + } + rs.redirectTo = url + return rs +} + +func (rs *Response) castBasicVarsToString(data interface{}) string { + switch dataType := data.(type) { + case string: + return fmt.Sprintf("%v", data) + case []byte: + d := data.(string) + return fmt.Sprintf("%v", d) + case int: + intVar, _ := data.(int) + return fmt.Sprintf("%v", intVar) + case int8: + int8Var := data.(int8) + return fmt.Sprintf("%v", int8Var) + case int16: + int16Var := data.(int16) + return fmt.Sprintf("%v", int16Var) + case int32: + int32Var := data.(int32) + return fmt.Sprintf("%v", int32Var) + case int64: + int64Var := data.(int64) + return fmt.Sprintf("%v", int64Var) + case uint: + uintVar, _ := data.(uint) + return fmt.Sprintf("%v", uintVar) + case uint8: + uint8Var := data.(uint8) + return fmt.Sprintf("%v", uint8Var) + case uint16: + uint16Var := data.(uint16) + return fmt.Sprintf("%v", uint16Var) + case uint32: + uint32Var := data.(uint32) + return fmt.Sprintf("%v", uint32Var) + case uint64: + uint64Var := data.(uint64) + return fmt.Sprintf("%v", uint64Var) + case float32: + float32Var := data.(float32) + return fmt.Sprintf("%v", float32Var) + case float64: + float64Var := data.(float64) + return fmt.Sprintf("%v", float64Var) + case complex64: + complex64Var := data.(complex64) + return fmt.Sprintf("%v", complex64Var) + case complex128: + complex128Var := data.(complex128) + return fmt.Sprintf("%v", complex128Var) + case bool: + boolVar := data.(bool) + return fmt.Sprintf("%v", boolVar) + default: + panic(fmt.Sprintf("unsupported response data type %v!", dataType)) + } +} + +func (rs *Response) reset() { + rs.body = nil + rs.statusCode = http.StatusOK + rs.contentType = CONTENT_TYPE_HTML + rs.overrideContentType = "" + rs.isTerminated = false + rs.redirectTo = "" +} diff --git a/response_test.go b/response_test.go new file mode 100644 index 0000000..dd99405 --- /dev/null +++ b/response_test.go @@ -0,0 +1,154 @@ +package core + +import ( + "fmt" + "testing" +) + +func TestWrite(t *testing.T) { + res := Response{} + v := "test-text" + res.Any(v) + + if string(res.body) != v { + t.Errorf("failed writing text") + } +} + +func TestWriteJson(t *testing.T) { + res := Response{} + j := "{\"name\": \"test\"}" + res.Json(j) + + if string(res.body) != j { + t.Errorf("failed wrting jsom") + } +} + +func TestSetHeaders(t *testing.T) { + res := Response{} + res.SetHeader("testkey", "testval") + + headers := res.headers + if len(headers) < 1 { + t.Errorf("testing set header failed") + } +} + +func TestReset(t *testing.T) { + res := Response{} + res.Any("test text") + if res.body == nil { + t.Errorf("expecting body to not be empty, found empty") + } + j := "{\"name\": \"test\"}" + res.Json(j) + if string(res.body) == "" { + t.Errorf("expecting JsonBody to not be empty, found empty") + } + + res.reset() + + if !(res.body == nil && string(res.body) == "") { + t.Errorf("failed testing response reset()") + } +} + +func TestCastBasicVarToString(t *testing.T) { + s := "test str" + r := Response{} + c := r.castBasicVarsToString(s) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var i int = 3 + r = Response{} + c = r.castBasicVarsToString(i) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var i8 int8 = 3 + r = Response{} + c = r.castBasicVarsToString(i8) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var i16 int16 = 3 + r = Response{} + c = r.castBasicVarsToString(i16) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var i32 int32 = 3 + r = Response{} + c = r.castBasicVarsToString(i32) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var i64 int64 = 3 + r = Response{} + c = r.castBasicVarsToString(i64) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var ui uint = 3 + r = Response{} + c = r.castBasicVarsToString(ui) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var ui8 uint8 = 3 + r = Response{} + c = r.castBasicVarsToString(ui8) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var ui16 uint16 = 3 + r = Response{} + c = r.castBasicVarsToString(ui16) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var ui32 uint32 = 3 + r = Response{} + c = r.castBasicVarsToString(ui32) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var ui64 uint64 = 3 + r = Response{} + c = r.castBasicVarsToString(ui64) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var f32 float32 = 3 + r = Response{} + c = r.castBasicVarsToString(f32) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var f64 float64 = 3 + r = Response{} + c = r.castBasicVarsToString(f64) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var c64 complex64 = 3 + r = Response{} + c = r.castBasicVarsToString(c64) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var c128 complex128 = 3 + r = Response{} + c = r.castBasicVarsToString(c128) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } + var b bool = true + r = Response{} + c = r.castBasicVarsToString(b) + if fmt.Sprintf("%T", c) != "string" { + t.Errorf("failed test cast basic var to string") + } +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..5691540 --- /dev/null +++ b/router.go @@ -0,0 +1,104 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +type Route struct { + Method string + Path string + Handler Handler + Middlewares []Middleware +} + +type Router struct { + Routes []Route +} + +var router *Router + +func NewRouter() *Router { + router = &Router{ + []Route{}, + } + return router +} + +func ResolveRouter() *Router { + return router +} + +func (r *Router) Get(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: GET, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) Post(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: POST, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) Delete(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: DELETE, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) Patch(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: PATCH, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) Put(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: PUT, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) Options(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: OPTIONS, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) Head(path string, handler Handler, middlewares ...Middleware) *Router { + r.Routes = append(r.Routes, Route{ + Method: HEAD, + Path: path, + Handler: handler, + Middlewares: middlewares, + }) + return r +} + +func (r *Router) GetRoutes() []Route { + return r.Routes +} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..afcdf4c --- /dev/null +++ b/router_test.go @@ -0,0 +1,126 @@ +// Copyright 2021 Harran Ali . All rights reserved. +// Copyright (c) 2024 Zeni Kim +// Use of this source code is governed by MIT-style +// license that can be found in the LICENSE file. + +package core + +import ( + "fmt" + "testing" +) + +func TestNewRouter(t *testing.T) { + r := NewRouter() + + if fmt.Sprintf("%T", r) != "*core.Router" { + t.Error("failed asserting initiation of new router") + } +} + +func TestResolveRouter(t *testing.T) { + r := ResolveRouter() + if fmt.Sprintf("%T", r) != "*core.Router" { + t.Error("failed resolve router variable") + } +} + +func TestGetRequest(t *testing.T) { + r := NewRouter() + handler := Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + }) + r.Get("/", handler) + + route := r.GetRoutes()[0] + if route.Method != "get" || route.Path != "/" { + t.Errorf("failed adding route with get http method") + } +} + +func TestPostRequest(t *testing.T) { + r := NewRouter() + handler := Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + }) + r.Post("/", handler) + + route := r.GetRoutes()[0] + if route.Method != "post" || route.Path != "/" { + t.Errorf("failed adding route with post http method") + } +} + +func TestDeleteRequest(t *testing.T) { + r := NewRouter() + handler := Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + }) + r.Delete("/", handler) + + route := r.GetRoutes()[0] + if route.Method != "delete" || route.Path != "/" { + t.Errorf("failed adding route with delete http method") + } +} + +func TestPutRequest(t *testing.T) { + r := NewRouter() + handler := Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + }) + r.Put("/", handler) + + route := r.GetRoutes()[0] + if route.Method != "put" || route.Path != "/" { + t.Errorf("failed adding route with put http method") + } +} + +func TestOptionsRequest(t *testing.T) { + r := NewRouter() + handler := Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + }) + r.Options("/", handler) + + route := r.GetRoutes()[0] + if route.Method != "options" || route.Path != "/" { + t.Errorf("failed adding route with options http method") + } +} + +func TestHeadRequest(t *testing.T) { + r := NewRouter() + handler := Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + }) + r.Head("/", handler) + + route := r.GetRoutes()[0] + if route.Method != "head" || route.Path != "/" { + t.Errorf("failed adding route with head http method") + } +} + +func TestAddMultipleRoutes(t *testing.T) { + r := NewRouter() + r.Get("/", Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + })) + r.Post("/", Handler(func(c *Context) *Response { + c.GetLogger().Info(TEST_STR) + return nil + })) + + if len(r.GetRoutes()) != 2 { + t.Errorf("failed getting added routes") + } +} diff --git a/testing-config.go b/testing-config.go new file mode 100644 index 0000000..90545d9 --- /dev/null +++ b/testing-config.go @@ -0,0 +1,5 @@ +package core + +var testingRequestC = RequestConfig{ + MaxUploadFileSize: 20000000, // 20MB +} diff --git a/testingdata/.env b/testingdata/.env new file mode 100644 index 0000000..9311340 --- /dev/null +++ b/testingdata/.env @@ -0,0 +1,10 @@ +################################# +### Env Vars ### +################################# +KEY_ONE=VAL_ONE +KEY_TWO=VAL_TWO + + +# SQLITE +SQLITE_DB=testingdata/db.sqlite + diff --git a/testingdata/db.sqlite b/testingdata/db.sqlite new file mode 100644 index 0000000..e69de29 diff --git a/testingdata/testdata.json b/testingdata/testdata.json new file mode 100644 index 0000000..a89d62b --- /dev/null +++ b/testingdata/testdata.json @@ -0,0 +1,3 @@ +{ + "testKey": "testVal" +} diff --git a/testingdata/totestcopyfile.md b/testingdata/totestcopyfile.md new file mode 100644 index 0000000..d710de3 --- /dev/null +++ b/testingdata/totestcopyfile.md @@ -0,0 +1 @@ +test copy file \ No newline at end of file diff --git a/testingdata/totestmovefile.md b/testingdata/totestmovefile.md new file mode 100644 index 0000000..a0b7a38 --- /dev/null +++ b/testingdata/totestmovefile.md @@ -0,0 +1 @@ +test move file \ No newline at end of file diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..7d8eac8 --- /dev/null +++ b/validator.go @@ -0,0 +1,242 @@ +package core + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/go-ozzo/ozzo-validation/is" +) + +type Validator struct{} +type validationResult struct { + hasFailed bool + errorMessages map[string]string +} + +var vr validationResult +var v *Validator + +func newValidator() *Validator { + v := &Validator{} + return v +} + +func resolveValidator() *Validator { + return v +} + +func (v *Validator) Validate(data map[string]interface{}, rules map[string]interface{}) validationResult { + vr = validationResult{} + vr.hasFailed = false + res := map[string]string{} + for key, val := range data { + _, ok := rules[key] + if !ok { + continue + } + rls, err := parseRules(rules[key]) + if err != nil { + panic(err.Error()) + } + err = validation.Validate(val, rls...) + if err != nil { + res[key] = fmt.Sprintf("%v: %v", key, err.Error()) + + } + } + if len(res) != 0 { + vr.hasFailed = true + vr.errorMessages = res + } + return vr +} + +func (vr *validationResult) Failed() bool { + return vr.hasFailed +} + +func (vr *validationResult) GetErrorMessagesMap() map[string]string { + return vr.errorMessages +} + +func (vr *validationResult) GetErrorMessagesJson() string { + j, err := json.Marshal(vr.GetErrorMessagesMap()) + if err != nil { + panic("error converting validation error messages to json") + } + return string(j) +} + +func parseRules(rawRules interface{}) ([]validation.Rule, error) { + var res []validation.Rule + rulesStr, ok := rawRules.(string) + if !ok { + return nil, errors.New("invalid validation rule") + } + rules := strings.Split(rulesStr, "|") + for _, rule := range rules { + rule = strings.TrimSpace(rule) + r, err := getRule(rule) + if err != nil { + return nil, err + } + res = append(res, r) + } + return res, nil +} + +func getRule(rule string) (validation.Rule, error) { + switch { + case strings.Contains(rule, "max:"): + return getRuleMax(rule) + case strings.Contains(rule, "min:"): + return getRuleMin(rule) + case strings.Contains(rule, "in:"): + return getRuleIn(rule) + case strings.Contains(rule, "dateLayout:"): + return getRuleDateLayout(rule) + case strings.Contains(rule, "length:"): + return getRuleLength(rule) + } + + switch rule { + case "required": + return validation.Required, nil + case "email": + return is.Email, nil + case "url": + return is.URL, nil + case "alpha": + return is.Alpha, nil + case "digit": + return is.Digit, nil + case "alphaNumeric": + return is.Alphanumeric, nil + case "lowerCase": + return is.LowerCase, nil + case "upperCase": + return is.UpperCase, nil + case "int": + return is.Int, nil + case "float": + return is.Float, nil + case "uuid": + return is.UUID, nil + case "creditCard": + return is.CreditCard, nil + case "json": + return is.JSON, nil + case "base64": + return is.Base64, nil + case "countryCode2": + return is.CountryCode2, nil + case "countryCode3": + return is.CountryCode3, nil + case "isoCurrencyCode": + return is.CurrencyCode, nil + case "mac": + return is.MAC, nil + case "ip": + return is.IP, nil + case "ipv4": + return is.IPv4, nil + case "ipv6": + return is.IPv6, nil + case "subdomain": + return is.Subdomain, nil + case "domain": + return is.Domain, nil + case "dnsName": + return is.DNSName, nil + case "host": + return is.Host, nil + case "port": + return is.Port, nil + case "mongoID": + return is.MongoID, nil + case "latitude": + return is.Latitude, nil + case "longitude": + return is.Longitude, nil + case "ssn": + return is.SSN, nil + case "semver": + return is.Semver, nil + default: + err := errors.New(fmt.Sprintf("invalid validation rule: %v", rule)) + return nil, err + } +} + +func getRuleMax(rule string) (validation.Rule, error) { + // max: 44 + rr := strings.ReplaceAll(rule, "max:", "") + m := strings.TrimSpace(rr) + n, err := strconv.ParseInt(m, 10, 64) + if err != nil { + err := errors.New("invalid value for validation rule 'max'") + return nil, err + } + return validation.Max(n), err +} + +func getRuleMin(rule string) (validation.Rule, error) { + // min: 33 + rr := strings.ReplaceAll(rule, "min:", "") + m := strings.TrimSpace(rr) + n, err := strconv.ParseInt(m, 10, 64) + if err != nil { + err := errors.New("invalid value for validation rule 'min'") + return nil, err + } + return validation.Min(n), nil +} + +func getRuleIn(rule string) (validation.Rule, error) { + // in: first, second, third + var readyElms []interface{} + rr := strings.ReplaceAll(rule, "in:", "") + elms := strings.Split(rr, ",") + for _, elm := range elms { + readyElms = append(readyElms, strings.TrimSpace(elm)) + } + return validation.In(readyElms...), nil +} + +// example date layouts: https://programming.guide/go/format-parse-string-time-date-example.html +func getRuleDateLayout(rule string) (validation.Rule, error) { + // dateLayout: 02 January 2006 + rr := rule + rr = strings.TrimSpace(strings.Replace(rr, "dateLayout:", "", -1)) + return validation.Date(rr), nil +} + +func getRuleLength(rule string) (validation.Rule, error) { + // length: 3, 7 + rr := rule + rr = strings.Replace(rr, "length:", "", -1) + lengthRange := strings.Split(rr, ",") + if len(lengthRange) < 0 { + err := errors.New("min value is not set for validation rule 'length'") + return nil, err + } + min, err := strconv.Atoi(strings.TrimSpace(lengthRange[0])) + if err != nil { + err := errors.New("min value is not set for validation rule 'length'") + return nil, err + } + if len(lengthRange) < 1 { + err := errors.New("max value is not set for validation rule 'length'") + return nil, err + } + max, err := strconv.Atoi(strings.TrimSpace(lengthRange[1])) + if err != nil { + err := errors.New("max value is not set for validation rule 'length'") + return nil, err + } + return validation.Length(min, max), nil +} diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 0000000..1bc0e14 --- /dev/null +++ b/validator_test.go @@ -0,0 +1,358 @@ +package core + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "testing" + + "github.com/brianvoe/gofakeit/v6" + "github.com/google/uuid" +) + +type ruleTestData struct { + ruleName string + correctValue interface{} + correctValueExpectedResult interface{} + incorrectValue interface{} + incorrectValueExpectedResult error +} + +type rulesTestData []ruleTestData + +var rulesTestDataList = []ruleTestData{ + { + ruleName: "required", + correctValue: gofakeit.Name(), + correctValueExpectedResult: nil, + incorrectValue: "", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "email", + correctValue: gofakeit.Email(), + correctValueExpectedResult: nil, + incorrectValue: "test@mailcom", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "url", + correctValue: gofakeit.URL(), + correctValueExpectedResult: nil, + incorrectValue: "http:/githubcom/goffe/ggoffeor", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "alpha", + correctValue: gofakeit.LoremIpsumWord(), + correctValueExpectedResult: nil, + incorrectValue: "test232", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "digit", + correctValue: gofakeit.Digit(), + correctValueExpectedResult: nil, + incorrectValue: "d", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "alphaNumeric", + correctValue: "abc3", + correctValueExpectedResult: nil, + incorrectValue: "!", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "lowerCase", + correctValue: "abc", + correctValueExpectedResult: nil, + incorrectValue: "ABC", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "upperCase", + correctValue: "ABC", + correctValueExpectedResult: nil, + incorrectValue: "abc", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "int", + correctValue: "343", + correctValueExpectedResult: nil, + incorrectValue: "342.3", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "float", + correctValue: "433.5", + correctValueExpectedResult: nil, + incorrectValue: gofakeit.LoremIpsumWord(), + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "uuid", + correctValue: uuid.NewString(), + correctValueExpectedResult: nil, + incorrectValue: gofakeit.LoremIpsumWord(), + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "creditCard", + correctValue: "4242 4242 4242 4242", + correctValueExpectedResult: nil, + incorrectValue: "dd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "json", + correctValue: "{\"testKEy\": \"testVal\"}", + correctValueExpectedResult: nil, + incorrectValue: "dd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "base64", + correctValue: "+rxVsR0pD0DU4XO4MZbXXg==", + correctValueExpectedResult: nil, + incorrectValue: "dd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "countryCode2", + correctValue: "SD", + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "countryCode3", + correctValue: "SDN", + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "isoCurrencyCode", + correctValue: "USD", + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "mac", + correctValue: gofakeit.MacAddress(), + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "ip", + correctValue: gofakeit.IPv4Address(), + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "ipv4", + correctValue: gofakeit.IPv4Address(), + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "ipv6", + correctValue: gofakeit.IPv6Address(), + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "domain", + correctValue: "site.com", + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "latitude", + correctValue: fmt.Sprintf("%v", gofakeit.Latitude()), + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "longitude", + correctValue: fmt.Sprintf("%v", gofakeit.Longitude()), + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, + { + ruleName: "semver", + correctValue: "3.2.4", + correctValueExpectedResult: nil, + incorrectValue: "ddd", + incorrectValueExpectedResult: errors.New("mock error"), + }, +} + +func TestValidatorValidate(t *testing.T) { + validator := newValidator() + v := validator.Validate(map[string]interface{}{ + "name": gofakeit.LoremIpsumWord(), + "link": gofakeit.URL(), + }, + map[string]interface{}{ + "name": "required|alphaNumeric", + "link": "required|url", + }, + ) + if v.Failed() { + t.Errorf("erro testing validator validate: '%v'", v.GetErrorMessagesJson()) + } + v = validator.Validate(map[string]interface{}{ + "name": "", + "link": gofakeit.URL(), + }, + map[string]interface{}{ + "name": "required|alphaNumeric", + "link": "required|url", + }, + ) + if !v.Failed() { + t.Errorf("erro testing validator validate") + } + msgsMap := v.GetErrorMessagesMap() + if !strings.Contains(msgsMap["name"], "cannot be blank") { + t.Errorf("erro testing validator validate") + } + msgsJson := v.GetErrorMessagesJson() + var masgsMapOfJ map[string]interface{} + json.Unmarshal([]byte(msgsJson), &masgsMapOfJ) + sval, _ := masgsMapOfJ["name"].(string) + if !strings.Contains(sval, "cannot be blank") { + t.Errorf("erro testing validator validate") + } +} + +func TestValidatorParseRules(t *testing.T) { + _, err := parseRules("344") + if err == nil { + t.Errorf("failed testing validation parse rules") + } + rules, err := parseRules("required| min: 3|length: 3, 5") + if err != nil { + t.Errorf("failed testing validation parse rules: '%v'", err.Error()) + } + if len(rules) != 3 { + t.Errorf("failed testing validation parse rules") + } +} + +func TestValidatorGetRule(t *testing.T) { + for _, td := range rulesTestDataList { + r, err := getRule(td.ruleName) + if err != nil { + t.Errorf("failed testing validation rule '%v'", td.ruleName) + } + err = r.Validate(td.correctValue) + if err != nil { + t.Errorf("failed testing validation rule '%v'", td.ruleName) + } + err = r.Validate(td.incorrectValue) + if err == nil { + t.Errorf("failed testing validation rule %v", td.ruleName) + } + } + _, err := getRule("unknownrule") + if err == nil { + t.Errorf("failed testing validation rule") + } +} + +func TestValidatorGetRuleMax(t *testing.T) { + r, err := getRuleMax("max: 33") + if err != nil { + t.Errorf("failed testing validation rule 'max'") + } + err = r.Validate(30) + if err != nil { + t.Errorf("failed testing validation rule 'max': %v", err.Error()) + } + err = r.Validate(40) + if err == nil { + t.Errorf("failed testing validation rule 'max'") + } +} + +func TestValidatorGetRuleMin(t *testing.T) { + r, err := getRuleMin("min: 33") + if err != nil { + t.Errorf("failed testing validation rule 'min'") + } + err = r.Validate(34) + if err != nil { + t.Errorf("failed testing validation rule 'min': %v", err.Error()) + } + err = r.Validate(3) + if err == nil { + t.Errorf("failed testing validation rule 'min'") + } +} + +func TestValidatorGetRuleIn(t *testing.T) { + r, err := getRuleIn("in: a, b, c") + if err != nil { + t.Errorf("failed testing validation rule 'in'") + } + err = r.Validate("a") + if err != nil { + t.Errorf("failed testing validation rule 'in': %v", err) + } +} + +func TestGetValidationRuleDateLayout(t *testing.T) { + r, err := getRuleDateLayout("dateLayout: 02 January 2006") + if err != nil { + t.Errorf("failed testing validation rule 'dateLayout'") + } + err = r.Validate("02 May 2023") + if err != nil { + t.Errorf("failed testing validation rule 'dateLayout': %v", err.Error()) + } + err = r.Validate("02-04-2023") + if err == nil { + t.Errorf("failed testing validation rule 'dateLayout'") + } +} + +func TestValidatorGetRuleLenth(t *testing.T) { + r, err := getRuleLength("length: 3, 5") + if err != nil { + t.Errorf("failed test validation rule 'length': %v", err.Error()) + } + err = r.Validate("123") + if err != nil { + t.Errorf("failed test validation rule 'length': %v", err.Error()) + } + err = r.Validate("12") + if err == nil { + t.Errorf("failed test validation rule 'length'") + } + err = r.Validate("123456") + if err == nil { + t.Errorf("failed test validation rule 'length'") + } + + r, err = getRuleLength("length: 3dd, 5") + if err == nil { + t.Errorf("failed test validation rule 'length'") + } + r, err = getRuleLength("length: 3, 5dd") + if err == nil { + t.Errorf("failed test validation rule 'length'") + } +}