From ecf91f6920799635aa946bddfc80bd15abb19805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Wed, 4 Sep 2024 08:41:18 +0800 Subject: [PATCH] feat: sync latest framework changes (#20) * feat: sync latest framework changes * feat: add rate limiter example * fix: lint * fix: lint * fix: lint * fix: lint * feat: add throttle test * feat: optimize throttle test * feat: bump postgres version to 16 * feat: modify port * fix: test * fix: test * optimize test * optimize test * optimize test * optimize test --------- Co-authored-by: Bowen --- .env | 4 +- .env.example | 7 +- .github/workflows/test.yml | 9 -- Dockerfile | 7 +- README.md | 6 +- README_zh.md | 6 +- app/http/kernel.go | 2 + app/providers/route_service_provider.go | 20 +++- config/app.go | 23 ++-- config/cache.go | 2 +- config/cors.go | 2 +- config/database.go | 2 +- config/filesystems.go | 11 +- config/grpc.go | 4 +- config/hashing.go | 26 ++-- config/http.go | 3 + config/jwt.go | 2 +- config/mail.go | 2 +- config/route.go | 25 ---- main.go | 6 +- resources/views/css/index.css | 4 +- resources/views/index.html | 6 +- routes/web.go | 3 +- tests/controllers/lang_controller_test.go | 66 ----------- tests/controllers/main_test.go | 29 ----- .../controllers/validation_controller_test.go | 68 ----------- tests/{controllers => feature}/lang/cn.json | 0 tests/{controllers => feature}/lang/en.json | 0 tests/feature/route_test.go | 112 ++++++++++++++++-- 29 files changed, 193 insertions(+), 264 deletions(-) delete mode 100644 config/route.go delete mode 100644 tests/controllers/lang_controller_test.go delete mode 100644 tests/controllers/main_test.go delete mode 100644 tests/controllers/validation_controller_test.go rename tests/{controllers => feature}/lang/cn.json (100%) rename tests/{controllers => feature}/lang/en.json (100%) diff --git a/.env b/.env index 208cace..6e6334f 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ APP_NAME=Goravel APP_ENV=local -APP_KEY=h20Gmg7UIuUDD66gfUSUjIkPLU8KLqMK +APP_KEY=ABCDEFGHIJKLMNOPQRSTUVWXYZ123456 APP_DEBUG=true APP_URL=http://localhost APP_HOST=127.0.0.1 @@ -9,7 +9,7 @@ APP_PORT=3000 GRPC_HOST=127.0.0.1 GRPC_PORT=3001 -JWT_SECRET=L9qknFZyWHDSVXXe1TjUsR7XQsxkbb8B +JWT_SECRET=ABCDEFGHIJKLMNOPQRSTUVWXYZ123456 LOG_CHANNEL=stack LOG_LEVEL=debug diff --git a/.env.example b/.env.example index 73edca2..ac871a9 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,8 @@ APP_URL=http://localhost APP_HOST=127.0.0.1 APP_PORT=3000 -GRPC_HOST=127.0.0.1 -GRPC_PORT=3001 +GRPC_HOST= +GRPC_PORT= JWT_SECRET= @@ -21,6 +21,9 @@ DB_DATABASE=goravel DB_USERNAME=root DB_PASSWORD= +SESSION_DRIVER=file +SESSION_LIFETIME=120 + REDIS_HOST=127.0.0.1 REDIS_PASSWORD= REDIS_PORT=6379 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 006cdad..ea2965b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,13 +11,6 @@ jobs: go: [ "1.21", "1.22" ] runs-on: ubuntu-latest steps: -# - name: Set up PostgreSQL -# uses: harmon758/postgresql-action@v1 -# with: -# postgresql version: '11' -# postgresql db: 'goravel' -# postgresql user: 'goravel' -# postgresql password: 'goravel' - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: @@ -33,7 +26,5 @@ jobs: ${{ runner.os }}-go- - name: Install dependencies run: go mod tidy - - name: Run migrate - run: go run . artisan migrate - name: Run tests run: go test -timeout 1h ./... diff --git a/Dockerfile b/Dockerfile index 5d2f9eb..4e92231 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.18.3-alpine3.16 AS builder +FROM golang:alpine AS builder ENV GO111MODULE=on \ CGO_ENABLED=0 \ @@ -10,7 +10,7 @@ COPY . . RUN go mod tidy RUN go build --ldflags "-extldflags -static" -o main . -FROM alpine:3.16 +FROM alpine:latest WORKDIR /www @@ -18,6 +18,7 @@ COPY --from=builder /build/main /www/ COPY --from=builder /build/database/ /www/database/ COPY --from=builder /build/public/ /www/public/ COPY --from=builder /build/storage/ /www/storage/ +COPY --from=builder /build/resources/ /www/resources/ COPY --from=builder /build/.env /www/.env -ENTRYPOINT ["/www/main"] \ No newline at end of file +ENTRYPOINT ["/www/main"] diff --git a/README.md b/README.md index 34bc604..3d17212 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,15 @@ Welcome to star, PR and issues! ### Integration of single page application into the framework -[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L41) +[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L44) ### View nesting -[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L52) +[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L53) ### Localization -[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L60) +[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L61) ### Session diff --git a/README_zh.md b/README_zh.md index 9f2cda0..fb8cb46 100644 --- a/README_zh.md +++ b/README_zh.md @@ -55,15 +55,15 @@ Laravel! ### 单页面前端应用集成到框架 -[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L43) +[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L44) ### 视图嵌套 -[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L52) +[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L53) ### 本地化 -[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L60) +[routes/web.go](https://github.com/goravel/example/blob/master/routes/web.go#L61) ### Session diff --git a/app/http/kernel.go b/app/http/kernel.go index 9c960f8..58a748d 100644 --- a/app/http/kernel.go +++ b/app/http/kernel.go @@ -2,6 +2,7 @@ package http import ( "github.com/goravel/framework/contracts/http" + httpmiddleware "github.com/goravel/framework/http/middleware" "github.com/goravel/framework/session/middleware" ) @@ -12,6 +13,7 @@ type Kernel struct { // These middleware are run during every request to your application. func (kernel Kernel) Middleware() []http.Middleware { return []http.Middleware{ + httpmiddleware.Throttle("global"), middleware.StartSession(), } } diff --git a/app/providers/route_service_provider.go b/app/providers/route_service_provider.go index 1548443..f2d52de 100644 --- a/app/providers/route_service_provider.go +++ b/app/providers/route_service_provider.go @@ -2,7 +2,9 @@ package providers import ( "github.com/goravel/framework/contracts/foundation" + contractshttp "github.com/goravel/framework/contracts/http" "github.com/goravel/framework/facades" + "github.com/goravel/framework/http/limit" "goravel/app/http" "goravel/routes" @@ -15,9 +17,23 @@ func (receiver *RouteServiceProvider) Register(app foundation.Application) { } func (receiver *RouteServiceProvider) Boot(app foundation.Application) { - //Add HTTP middleware + // Add HTTP middleware facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...) - //Add routes + receiver.configureRateLimiting() + + // Add routes routes.Web() } + +func (receiver *RouteServiceProvider) configureRateLimiting() { + facades.RateLimiter().For("global", func(ctx contractshttp.Context) contractshttp.Limit { + return limit.PerMinute(1000) + }) + facades.RateLimiter().ForWithLimits("login", func(ctx contractshttp.Context) []contractshttp.Limit { + return []contractshttp.Limit{ + limit.PerDay(1000), + limit.PerMinute(5).By(ctx.Request().Ip()), + } + }) +} diff --git a/config/app.go b/config/app.go index de8461c..69c3973 100644 --- a/config/app.go +++ b/config/app.go @@ -20,6 +20,7 @@ import ( "github.com/goravel/framework/route" "github.com/goravel/framework/schedule" "github.com/goravel/framework/session" + "github.com/goravel/framework/support/carbon" "github.com/goravel/framework/testing" "github.com/goravel/framework/translation" "github.com/goravel/framework/validation" @@ -53,25 +54,31 @@ func init() { // Application Timezone // - // Here you may specify the default timezone for your application, which - // will be used by the PHP date and date-time functions. We have gone - // ahead and set this to a sensible default for you out of the box. - "timezone": "UTC", + // Here you may specify the default timezone for your application. + // Example: UTC, Asia/Shanghai + // More: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + "timezone": carbon.UTC, // Application Locale Configuration // // The application locale determines the default locale that will be used - // by the translation service provider.You are free to set this value + // by the translation service provider. You are free to set this value // to any of the locales which will be supported by the application. "locale": "en", // Application Fallback Locale // // The fallback locale determines the locale to use when the current one - // is not available.You may change the value to correspond to any of + // is not available. You may change the value to correspond to any of // the language folders that are provided through your application. "fallback_locale": "cn", + // Application Lang Path + // + // The path to the language files for the application. You may change + // the path to a different directory if you would like to customize it. + "lang_path": "lang", + // Encryption Key // // 32 character string, otherwise these encrypted strings @@ -96,10 +103,10 @@ func init() { &grpc.ServiceProvider{}, &mail.ServiceProvider{}, &auth.ServiceProvider{}, + &hash.ServiceProvider{}, + &crypt.ServiceProvider{}, &filesystem.ServiceProvider{}, &validation.ServiceProvider{}, - &crypt.ServiceProvider{}, - &hash.ServiceProvider{}, &session.ServiceProvider{}, &translation.ServiceProvider{}, &testing.ServiceProvider{}, diff --git a/config/cache.go b/config/cache.go index 3f16112..07df590 100644 --- a/config/cache.go +++ b/config/cache.go @@ -19,7 +19,7 @@ func init() { // Here you may define all the cache "stores" for your application as // well as their drivers. You may even define multiple stores for the // same cache driver to group types of items stored in your caches. - // Available Drivers: "memory", "redis", "custom" + // Available Drivers: "memory", "custom" "stores": map[string]any{ "memory": map[string]any{ "driver": "memory", diff --git a/config/cors.go b/config/cors.go index 8ad608e..0b389b9 100644 --- a/config/cors.go +++ b/config/cors.go @@ -18,7 +18,7 @@ func init() { "allowed_methods": []string{"*"}, "allowed_origins": []string{"*"}, "allowed_headers": []string{"*"}, - "exposed_headers": []string{"*"}, + "exposed_headers": []string{""}, "max_age": 0, "supports_credentials": false, }) diff --git a/config/database.go b/config/database.go index 6ad7081..ad9d9ff 100644 --- a/config/database.go +++ b/config/database.go @@ -7,7 +7,7 @@ import ( func init() { config := facades.Config() config.Add("database", map[string]any{ - // Default database connection name, only support Mysql now. + // Default database connection name "default": config.Env("DB_CONNECTION", "mysql"), // Database connections diff --git a/config/filesystems.go b/config/filesystems.go index 8645e3b..8cfea50 100644 --- a/config/filesystems.go +++ b/config/filesystems.go @@ -2,6 +2,7 @@ package config import ( "github.com/goravel/framework/facades" + "github.com/goravel/framework/support/path" ) func init() { @@ -20,12 +21,16 @@ func init() { // may even configure multiple disks of the same driver. Defaults have // been set up for each driver as an example of the required values. // - // Supported Drivers: "local", "s3", "oss", "cos", "custom" + // Supported Drivers: "local", "custom" "disks": map[string]any{ "local": map[string]any{ "driver": "local", - "root": "storage/app", - "url": config.Env("APP_URL").(string) + "/storage", + "root": path.Storage("app"), + }, + "public": map[string]any{ + "driver": "local", + "root": path.Storage("app/public"), + "url": config.Env("APP_URL", "").(string) + "/storage", }, }, }) diff --git a/config/grpc.go b/config/grpc.go index 1a88b6d..fa6e54a 100644 --- a/config/grpc.go +++ b/config/grpc.go @@ -7,10 +7,10 @@ import ( func init() { config := facades.Config() config.Add("grpc", map[string]any{ - // Grpc Configuration - // // Configure your server host "host": config.Env("GRPC_HOST", ""), + + // Configure your server port "port": config.Env("GRPC_PORT", ""), // Configure your client host and interceptors. diff --git a/config/hashing.go b/config/hashing.go index 9eec9a5..8852a00 100644 --- a/config/hashing.go +++ b/config/hashing.go @@ -11,10 +11,20 @@ func init() { // // This option controls the default diver that gets used // by the framework hash facade. - // Default driver is "argon2id", because it is the most secure. + // Default driver is "bcrypt". // - // Supported Drivers: "argon2id", "bcrypt" - "driver": "argon2id", + // Supported Drivers: "bcrypt", "argon2id" + "driver": "bcrypt", + + // Bcrypt Hashing Options + // rounds: The cost factor that should be used to compute the bcrypt hash. + // The cost factor controls how much time is needed to compute a single bcrypt hash. + // The higher the cost factor, the more hashing rounds are done. Increasing the cost + // factor by 1 doubles the necessary time. After a certain point, the returns on + // hashing time versus attacker time are diminishing, so choose your cost factor wisely. + "bcrypt": map[string]any{ + "rounds": 12, + }, // Argon2id Hashing Options // memory: A memory cost, which defines the memory usage, given in kibibytes. @@ -26,15 +36,5 @@ func init() { "time": 4, "threads": 1, }, - - // Bcrypt Hashing Options - // rounds: The cost factor that should be used to compute the bcrypt hash. - // The cost factor controls how much time is needed to compute a single bcrypt hash. - // The higher the cost factor, the more hashing rounds are done. Increasing the cost - // factor by 1 doubles the necessary time. After a certain point, the returns on - // hashing time versus attacker time are diminishing, so choose your cost factor wisely. - "bcrypt": map[string]any{ - "rounds": 10, - }, }) } diff --git a/config/http.go b/config/http.go index 2a9e192..7bca7e6 100644 --- a/config/http.go +++ b/config/http.go @@ -17,6 +17,9 @@ func init() { // HTTP Drivers "drivers": map[string]any{ "gin": map[string]any{ + // Optional, default is 4096 KB + "body_limit": 4096, + "header_limit": 4096, "route": func() (route.Route, error) { return ginfacades.Route("gin"), nil }, diff --git a/config/jwt.go b/config/jwt.go index 79370f4..7c1f032 100644 --- a/config/jwt.go +++ b/config/jwt.go @@ -32,7 +32,7 @@ func init() { // the original token being created until they must re-authenticate. // Defaults to 2 weeks. // - // You can also set this to null, to yield an infinite refresh time. + // You can also set this to 0, to yield an infinite refresh time. // Some may want this instead of never expiring tokens for e.g. a mobile app. // This is not particularly recommended, so make sure you have appropriate // systems in place to revoke the token if necessary. diff --git a/config/mail.go b/config/mail.go index cdef1f8..0439cd6 100644 --- a/config/mail.go +++ b/config/mail.go @@ -4,7 +4,7 @@ import "github.com/goravel/framework/facades" func init() { config := facades.Config() - facades.Config().Add("mail", map[string]any{ + config.Add("mail", map[string]any{ // SMTP Host Address // // Here you may provide the host address of the SMTP server used by your diff --git a/config/route.go b/config/route.go deleted file mode 100644 index 90c2667..0000000 --- a/config/route.go +++ /dev/null @@ -1,25 +0,0 @@ -package config - -import ( - "github.com/goravel/framework/facades" -) - -func init() { - config := facades.Config() - config.Add("route", map[string]any{ - // HTTP Host - "host": config.Env("APP_HOST", "127.0.0.1:3000"), - // HTTPS Configuration - "tls": map[string]any{ - // HTTPS Host - "host": config.Env("APP_HOST", "127.0.0.1:3000"), - // SSL Certificate - "ssl": map[string]any{ - // ca.pem - "cert": "", - // ca.key - "key": "", - }, - }, - }) -} diff --git a/main.go b/main.go index 5158e2f..1194767 100644 --- a/main.go +++ b/main.go @@ -10,17 +10,17 @@ func main() { // This bootstraps the framework and gets it ready for use. bootstrap.Boot() - // Start HTTP server by facades.Route(). + // Start http server by facades.Route(). go func() { if err := facades.Route().Run(); err != nil { facades.Log().Errorf("Route run error: %v", err) } }() - // Start GRPC server + // Start grpc server by facades.Grpc(). go func() { if err := facades.Grpc().Run(); err != nil { - facades.Log().Errorf("Run grpc error: %+v", err) + facades.Log().Errorf("Grpc run error: %v", err) } }() diff --git a/resources/views/css/index.css b/resources/views/css/index.css index 724679b..b4ea16b 100644 --- a/resources/views/css/index.css +++ b/resources/views/css/index.css @@ -1 +1,3 @@ -.title {font-style:italic;} \ No newline at end of file +.title { + font-style: italic; +} \ No newline at end of file diff --git a/resources/views/index.html b/resources/views/index.html index b6704e8..869d90d 100644 --- a/resources/views/index.html +++ b/resources/views/index.html @@ -1,9 +1,9 @@ -Goravel Single Page Application Example - + Goravel Single Page Application Example + -

Goravel Single Page Application Example

+

Goravel Single Page Application Example

\ No newline at end of file diff --git a/routes/web.go b/routes/web.go index 1c9c232..cd2be74 100644 --- a/routes/web.go +++ b/routes/web.go @@ -4,6 +4,7 @@ import ( "github.com/goravel/framework/contracts/http" "github.com/goravel/framework/contracts/route" "github.com/goravel/framework/facades" + httpmiddleware "github.com/goravel/framework/http/middleware" "github.com/spf13/cast" "goravel/app/http/controllers" @@ -33,7 +34,7 @@ func Web() { // JWT jwtController := controllers.NewJwtController() - facades.Route().Get("/jwt/login", jwtController.Login) + facades.Route().Middleware(httpmiddleware.Throttle("login")).Get("/jwt/login", jwtController.Login) facades.Route().Middleware(middleware.Jwt()).Get("/jwt", jwtController.Index) // Swagger diff --git a/tests/controllers/lang_controller_test.go b/tests/controllers/lang_controller_test.go deleted file mode 100644 index 492c715..0000000 --- a/tests/controllers/lang_controller_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package controllers - -import ( - "fmt" - "io" - "net/http" - "testing" - - "github.com/stretchr/testify/suite" - - "goravel/tests" -) - -/* -***************************************** -We need add the lang folder in the testing package for now, will optimize it in v1.15 -***************************************** -*/ -type LangControllerTestSuite struct { - suite.Suite - tests.TestCase -} - -func TestLangControllerTestSuite(t *testing.T) { - suite.Run(t, &LangControllerTestSuite{}) -} - -// SetupTest will run before each test in the suite. -func (s *LangControllerTestSuite) SetupTest() { -} - -// TearDownTest will run after each test in the suite. -func (s *LangControllerTestSuite) TearDownTest() { -} - -func (s *LangControllerTestSuite) TestIndex() { - tests := []struct { - name string - lang string - expectResponse string - }{ - { - name: "use default lang", - expectResponse: "{\"current_locale\":\"en\",\"fallback\":\"Goravel 是一个基于 Go 语言的 Web 开发框架\",\"name\":\"Goravel Framework\"}", - }, - { - name: "lang is cn", - lang: "cn", - expectResponse: "{\"current_locale\":\"cn\",\"fallback\":\"Goravel 是一个基于 Go 语言的 Web 开发框架\",\"name\":\"Goravel 框架\"}", - }, - } - - for _, test := range tests { - s.Run(test.name, func() { - resp, err := http.Get(route(fmt.Sprintf("/lang?lang=%s", test.lang))) - s.Require().NoError(err) - - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - - s.Require().NoError(err) - s.Equal(http.StatusOK, resp.StatusCode) - s.Equal(test.expectResponse, string(body)) - }) - } -} diff --git a/tests/controllers/main_test.go b/tests/controllers/main_test.go deleted file mode 100644 index c2d7d78..0000000 --- a/tests/controllers/main_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package controllers - -import ( - "fmt" - "testing" - - "github.com/goravel/framework/facades" - "github.com/goravel/framework/support/file" - "github.com/goravel/framework/support/str" -) - -func TestMain(m *testing.M) { - go func() { - if err := facades.Route().Run(); err != nil { - facades.Log().Errorf("Route run error: %v", err) - } - }() - - m.Run() - - file.Remove("storage") -} - -func route(path string) string { - return fmt.Sprintf("http://%s:%s/%s", - facades.Config().GetString("APP_HOST"), - facades.Config().GetString("APP_PORT"), - str.Of(path).LTrim("/").String()) -} diff --git a/tests/controllers/validation_controller_test.go b/tests/controllers/validation_controller_test.go deleted file mode 100644 index 3205c7d..0000000 --- a/tests/controllers/validation_controller_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package controllers - -import ( - "io" - "net/http" - "strings" - "testing" - - "github.com/stretchr/testify/suite" - - "goravel/tests" -) - -/* -***************************************** - 1. Please init .env file before running the test; - 2. Running the HTTP server in the mail_test.go file; - 3. An HTTP package(eg: net/http) is required for now, will optimize the test experience in this issue: - https://github.com/goravel/goravel/issues/441 - -***************************************** -*/ -type ValidationControllerTestSuite struct { - suite.Suite - tests.TestCase -} - -func TestValidationControllerTestSuite(t *testing.T) { - suite.Run(t, &ValidationControllerTestSuite{}) -} - -// SetupTest will run before each test in the suite. -func (s *ValidationControllerTestSuite) SetupTest() { -} - -// TearDownTest will run after each test in the suite. -func (s *ValidationControllerTestSuite) TearDownTest() { -} - -func (s *ValidationControllerTestSuite) TestJson() { - payload := strings.NewReader(`{ - "name": "Goravel", - "date": "2024-07-08 18:33:32" - }`) - resp, err := http.Post(route("/validation/json"), "application/json", payload) - s.Require().NoError(err) - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - s.Require().NoError(err) - s.Equal(http.StatusOK, resp.StatusCode) - s.Equal(string(body), "{\"date\":\"2024-07-08 18:33:32\",\"name\":\"Goravel\"}") -} - -func (s *ValidationControllerTestSuite) TestRequest() { - payload := strings.NewReader(`{ - "name": "Goravel", - "date": "2024-07-08 18:33:32", - "tags": ["tag1", "tag2"], - "scores": [1, 2] - }`) - resp, err := http.Post(route("/validation/request"), "application/json", payload) - s.Require().NoError(err) - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - s.Require().NoError(err) - s.Equal(http.StatusOK, resp.StatusCode) - s.Equal(string(body), "{\"date\":\"2024-07-08 18:33:32\",\"name\":\"Goravel\",\"scores\":[1,2],\"tags\":[\"tag1\",\"tag2\"]}") -} diff --git a/tests/controllers/lang/cn.json b/tests/feature/lang/cn.json similarity index 100% rename from tests/controllers/lang/cn.json rename to tests/feature/lang/cn.json diff --git a/tests/controllers/lang/en.json b/tests/feature/lang/en.json similarity index 100% rename from tests/controllers/lang/en.json rename to tests/feature/lang/en.json diff --git a/tests/feature/route_test.go b/tests/feature/route_test.go index 7c216d8..7b4ae28 100644 --- a/tests/feature/route_test.go +++ b/tests/feature/route_test.go @@ -3,6 +3,7 @@ package feature import ( "fmt" "net/http" + "strings" "testing" "github.com/go-resty/resty/v2" @@ -16,10 +17,17 @@ import ( type RouteTestSuite struct { suite.Suite tests.TestCase + http *resty.Request } func TestRouteTestSuite(t *testing.T) { - suite.Run(t, &RouteTestSuite{}) + suite.Run(t, &RouteTestSuite{ + http: resty.New(). + SetBaseURL(fmt.Sprintf("http://%s:%s", + facades.Config().GetString("APP_HOST"), + facades.Config().GetString("APP_PORT"))). + SetHeader("Content-Type", "application/json").R(), + }) } // SetupTest will run before each test in the suite. @@ -32,18 +40,12 @@ func (s *RouteTestSuite) TearDownTest() { } func (s *RouteTestSuite) TestUsers() { - client := resty.New(). - SetBaseURL(fmt.Sprintf("http://%s:%s", - facades.Config().GetString("APP_HOST"), - facades.Config().GetString("APP_PORT"))). - SetHeader("Content-Type", "application/json") - // Add a user var createdUser struct { User models.User } - resp, err := client.R().SetResult(&createdUser).SetBody(map[string]string{ + resp, err := s.http.SetResult(&createdUser).SetBody(map[string]string{ "name": "Goravel", "avatar": "https://goravel.dev/avatar.png", }).Post("users") @@ -58,7 +60,7 @@ func (s *RouteTestSuite) TestUsers() { var users struct { Users []models.User } - resp, err = client.R().SetResult(&users).Get("users") + resp, err = s.http.SetResult(&users).Get("users") s.Require().NoError(err) s.Require().Equal(http.StatusOK, resp.StatusCode()) @@ -72,7 +74,7 @@ func (s *RouteTestSuite) TestUsers() { User models.User } - resp, err = client.R().SetResult(&updatedUser).SetBody(map[string]string{ + resp, err = s.http.SetResult(&updatedUser).SetBody(map[string]string{ "name": "Framework", }).Put(fmt.Sprintf("users/%d", createdUser.User.ID)) @@ -86,7 +88,7 @@ func (s *RouteTestSuite) TestUsers() { var user struct { User models.User } - resp, err = client.R().SetResult(&user).Get(fmt.Sprintf("users/%d", createdUser.User.ID)) + resp, err = s.http.SetResult(&user).Get(fmt.Sprintf("users/%d", createdUser.User.ID)) s.Require().NoError(err) s.Require().Equal(http.StatusOK, resp.StatusCode()) @@ -95,16 +97,100 @@ func (s *RouteTestSuite) TestUsers() { s.Equal("https://goravel.dev/avatar.png", user.User.Avatar) // Delete the User - resp, err = client.R().Delete(fmt.Sprintf("users/%d", createdUser.User.ID)) + resp, err = s.http.Delete(fmt.Sprintf("users/%d", createdUser.User.ID)) s.Require().NoError(err) s.Require().Equal(http.StatusOK, resp.StatusCode()) s.Equal("{\"rows_affected\":1}", resp.String()) // Get Users - resp, err = client.R().Get("users") + resp, err = s.http.Get("users") s.Require().NoError(err) s.Require().Equal(http.StatusOK, resp.StatusCode()) s.Equal("{\"users\":[]}", resp.String()) } + +func (s *RouteTestSuite) TestLang() { + tests := []struct { + name string + lang string + expectResponse string + }{ + { + name: "use default lang", + expectResponse: "{\"current_locale\":\"en\",\"fallback\":\"Goravel 是一个基于 Go 语言的 Web 开发框架\",\"name\":\"Goravel Framework\"}", + }, + { + name: "lang is cn", + lang: "cn", + expectResponse: "{\"current_locale\":\"cn\",\"fallback\":\"Goravel 是一个基于 Go 语言的 Web 开发框架\",\"name\":\"Goravel 框架\"}", + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + resp, err := s.http.Get(fmt.Sprintf("/lang?lang=%s", test.lang)) + + s.NoError(err) + s.Equal(http.StatusOK, resp.StatusCode()) + s.Equal(test.expectResponse, resp.String()) + }) + } +} + +func (s *RouteTestSuite) TestValidationJson() { + payload := strings.NewReader(`{ + "name": "Goravel", + "date": "2024-07-08 18:33:32" + }`) + + resp, err := s.http.SetBody(payload).Post("/validation/json") + + s.NoError(err) + s.Equal(http.StatusOK, resp.StatusCode()) + s.Equal("{\"date\":\"2024-07-08 18:33:32\",\"name\":\"Goravel\"}", resp.String()) +} + +func (s *RouteTestSuite) TestValidationRequest() { + payload := strings.NewReader(`{ + "name": "Goravel", + "date": "2024-07-08 18:33:32", + "tags": ["tag1", "tag2"], + "scores": [1, 2] + }`) + + resp, err := s.http.SetBody(payload).Post("/validation/request") + + s.NoError(err) + s.Equal(http.StatusOK, resp.StatusCode()) + s.Equal("{\"date\":\"2024-07-08 18:33:32\",\"name\":\"Goravel\",\"scores\":[1,2],\"tags\":[\"tag1\",\"tag2\"]}", resp.String()) +} + +func (s *RouteTestSuite) TestThrottle() { + tests := []struct { + name string + expectStatusCode int + }{ + { + name: "no throttle", + expectStatusCode: 200, + }, + { + name: "throttle", + expectStatusCode: 429, + }, + } + + for _, test := range tests { + s.Run(test.name, func() { + var resp *resty.Response + var err error + for i := 0; i < 5; i++ { + resp, err = s.http.Get("/jwt/login") + s.Require().NoError(err) + } + s.Equal(test.expectStatusCode, resp.StatusCode()) + }) + } +}