diff --git a/README.md b/README.md index 5000433..1346717 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Visit [`http://localhost:8080`](http://localhost:8080) to see the response: {"message": "Welcome to Okapi!"} ``` -Visit [`http://localhost:8080/docs/`](http://localhost:8080/docs/) to se the documentation +Visit [`http://localhost:8080/docs/`](http://localhost:8080/docs/) to see the documentation --- diff --git a/go.mod b/go.mod index ca1b296..ee9db18 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,13 @@ require ( require ( github.com/getkin/kin-openapi v0.132.0 github.com/jkaninda/go-utils v0.1.1 + github.com/stretchr/testify v1.10.0 github.com/swaggo/http-swagger v1.3.4 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/swag v1.16.4 // indirect golang.org/x/net v0.40.0 // indirect diff --git a/okapi.go b/okapi.go index 5128dbc..d6008e3 100644 --- a/okapi.go +++ b/okapi.go @@ -575,7 +575,7 @@ func (o *Okapi) StartServer(server *http.Server) error { // Stop gracefully shuts down the Okapi server(s) func (o *Okapi) Stop() { - _, _ = fmt.Fprintf(DefaultWriter, "Gracefully shutting down HTTP server at %s\n", o.TLSServer.Addr) + _, _ = fmt.Fprintf(DefaultWriter, "Gracefully shutting down HTTP server at %s\n", o.Server.Addr) if err := o.Shutdown(o.Server); err != nil { o.logger.Error("Failed to shutdown HTTP server", slog.String("error", err.Error())) panic(err) diff --git a/okapi_test.go b/okapi_test.go index 4acdfaf..258996e 100644 --- a/okapi_test.go +++ b/okapi_test.go @@ -56,7 +56,7 @@ func TestStart(t *testing.T) { o := Default() o.Get("/", func(c Context) error { - return c.JSON(http.StatusOK, M{"message": "Welcome to Okapi!"}) + return c.OK(M{"message": "Welcome to Okapi!"}) }) basicAuth := BasicAuthMiddleware{ @@ -72,9 +72,20 @@ func TestStart(t *testing.T) { v1 := api.Group("/v1") v1.Use(customMiddleware) - v1.Get("/books", func(c Context) error { return c.JSON(http.StatusOK, books) }) + v1.Get("/books", func(c Context) error { return c.OK(books) }) v1.Get("/books/:id", show) + v2 := api.Group("/v2").Disable() + v2.Get("/books", func(c Context) error { return c.OK(books) }) + v2.Get("/books/:id", show) + + v1.Get("/any/*any", func(c Context) error { + return c.OK(M{"message": "Tested Any"}) + }) + v1.Get("/all/*", func(c Context) error { + return c.OK(M{"message": "Tested Any"}) + }) + go func() { if err := o.Start(); err != nil && !errors.Is(err, http.ErrServerClosed) { t.Errorf("Server failed to start: %v", err) @@ -87,6 +98,14 @@ func TestStart(t *testing.T) { assertStatus(t, "GET", "http://localhost:8080/", nil, "", http.StatusOK) assertStatus(t, "GET", "http://localhost:8080/api/v1/books", nil, "", http.StatusOK) assertStatus(t, "GET", "http://localhost:8080/api/v1/books/1", nil, "", http.StatusOK) + // Docs + assertStatus(t, "GET", "http://localhost:8080/openapi.json", nil, "", http.StatusOK) + + // API V2 + assertStatus(t, "GET", "http://localhost:8080/api/v2/books/1", nil, "", http.StatusNotFound) + // Any + assertStatus(t, "GET", "http://localhost:8080/api/v1/any/request", nil, "", http.StatusOK) + assertStatus(t, "GET", "http://localhost:8080/api/v1/all/request", nil, "", http.StatusOK) // Unauthorized admin Post body := `{"id":5,"name":"The Go Programming Language","price":30,"qty":100}` @@ -200,6 +219,7 @@ func show(c Context) error { func customMiddleware(next HandleFunc) HandleFunc { return func(c Context) error { + start := time.Now() slog.Info("Custom middleware executed", "path", c.Request.URL.Path, "method", c.Request.Method) // Call the next handler in the chain if err := next(c); err != nil { @@ -207,6 +227,7 @@ func customMiddleware(next HandleFunc) HandleFunc { slog.Error("Error in custom middleware", "error", err) return c.JSON(http.StatusInternalServerError, M{"error": "Internal Server Error"}) } + slog.Info("Request took", "duration", time.Since(start)) return nil } } diff --git a/util.go b/util.go index 3dc7947..6e9956b 100644 --- a/util.go +++ b/util.go @@ -69,18 +69,19 @@ func normalizeRoutePath(path string) string { // Remove double slashes path = strings.ReplaceAll(path, "//", "/") - // Convert path parameters from :param to {param} - re := regexp.MustCompile(`:(\w+)`) - path = re.ReplaceAllString(path, `{$1}`) - - // Convert wildcard /* or /*any to /{any:.*} + // Convert /*any or /* to /{any:.*} if strings.HasSuffix(path, "/*") { path = strings.TrimSuffix(path, "/*") + "/{any:.*}" - } else if strings.Contains(path, "/*") { - // Handle cases like /files/*path - path = strings.Replace(path, "/*", "/{any:.*}", 1) + } else if matched, _ := regexp.MatchString(`/\*\w+`, path); matched { + // Handle cases like /*any, /*path, etc. + re := regexp.MustCompile(`/\*\w+`) + path = re.ReplaceAllString(path, "/{any:.*}") } + // Convert path parameters from :param to {param} AFTER handling wildcards + re := regexp.MustCompile(`:(\w+)`) + path = re.ReplaceAllString(path, `{$1}`) + return path } diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..f02a948 --- /dev/null +++ b/util_test.go @@ -0,0 +1,65 @@ +/* + * MIT License + * + * Copyright (c) 2025 Jonas Kaninda + * + * 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. + */ + +package okapi + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNormalizeRoutePath(t *testing.T) { + input := "/users/:id" + result := normalizeRoutePath(input) + assert.Equal(t, "/users/{id}", result) + + input = "/*" + result = normalizeRoutePath(input) + assert.Equal(t, "/{any:.*}", result) + + input = "/*any" + result = normalizeRoutePath(input) + assert.Equal(t, "/{any:.*}", result) + + input = "/*any" + result = normalizeRoutePath(input) + assert.Equal(t, "/{any:.*}", result) + + input = "/*path" + result = normalizeRoutePath(input) + assert.Equal(t, "/{any:.*}", result) + +} + +func TestAllowOrigin(t *testing.T) { + origin := "http://localhost" + origins := []string{"https://test/com", "https:example.com", "http://localhost"} + + result := allowedOrigin(origins, origin) + assert.Equal(t, true, result) + + origins = append(origins, "*") + result = allowedOrigin(origins, origin) + assert.Equal(t, true, result) +}