From edbb62449b930aafd33f968ef89f7b8d9a9d5b55 Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 8 Jan 2026 14:07:58 +0800 Subject: [PATCH] fix: stabilize backend tests --- backend/app/commands/testx/testing.go | 51 ++++++- backend/app/http/super/v1/routes.gen.go | 36 ++--- backend/app/http/v1/routes.gen.go | 160 +++++++++++----------- backend/app/services/common.go | 2 +- backend/app/services/content.go | 22 +-- backend/app/services/content_test.go | 8 +- backend/app/services/coupon_test.go | 3 +- backend/app/services/creator.go | 12 +- backend/app/services/creator_test.go | 5 +- backend/app/services/notification.go | 17 ++- backend/app/services/notification_test.go | 13 +- backend/app/services/order.go | 2 +- backend/app/services/order_test.go | 2 +- backend/app/services/provider.gen.go | 9 +- backend/app/services/tenant.go | 7 +- backend/app/services/user_test.go | 14 +- backend/app/services/wallet.go | 3 +- backend/config.test.toml | 62 +++++++++ 18 files changed, 281 insertions(+), 147 deletions(-) create mode 100644 backend/config.test.toml diff --git a/backend/app/commands/testx/testing.go b/backend/app/commands/testx/testing.go index e5d9f3d..96024db 100644 --- a/backend/app/commands/testx/testing.go +++ b/backend/app/commands/testx/testing.go @@ -2,13 +2,16 @@ package testx import ( "context" + "os" "testing" jobs_args "quyun/v2/app/jobs/args" "quyun/v2/database" + "quyun/v2/database/models" "quyun/v2/providers/job" "quyun/v2/providers/jwt" "quyun/v2/providers/postgres" + "quyun/v2/providers/storage" "github.com/riverqueue/river" "go.ipao.vip/atom" @@ -26,6 +29,7 @@ func Default(providers ...container.ProviderContainer) container.Providers { postgres.DefaultProvider(), jwt.DefaultProvider(), job.DefaultProvider(), + storage.DefaultProvider(), testJobWorkersProvider(), database.DefaultProvider(), }, providers...) @@ -47,6 +51,22 @@ func (w *mediaAssetProcessTestWorker) Work(ctx context.Context, job *river.Job[j return nil } +type notificationTestWorker struct { + river.WorkerDefaults[jobs_args.NotificationArgs] +} + +func (w *notificationTestWorker) Work(ctx context.Context, job *river.Job[jobs_args.NotificationArgs]) error { + arg := job.Args + n := &models.Notification{ + UserID: arg.UserID, + Type: arg.Type, + Title: arg.Title, + Content: arg.Content, + IsRead: false, + } + return models.NotificationQuery.WithContext(ctx).Create(n) +} + func testJobWorkersProvider() container.ProviderContainer { return container.ProviderContainer{ Provider: func(opts ...opt.Option) error { @@ -60,6 +80,10 @@ func testJobWorkersProvider() container.ProviderContainer { if err := river.AddWorkerSafely(__job.Workers, obj2); err != nil { return nil, err } + obj3 := ¬ificationTestWorker{} + if err := river.AddWorkerSafely(__job.Workers, obj3); err != nil { + return nil, err + } return obj, nil }, atom.GroupInitial) }, @@ -76,13 +100,30 @@ func Serve(providers container.Providers, t *testing.T, invoke any) { So(container.Container.Provide(func() context.Context { return context.Background() }), ShouldBeNil) file := fabfile.MustFind("config.toml") - - // localEnv := os.Getenv("ENV_LOCAL") - // if localEnv != "" { - // file = fabfile.MustFind("config." + localEnv + ".toml") - // } + // 支持通过 ENV_LOCAL 指定测试环境配置:config..toml + localEnv := os.Getenv("ENV_LOCAL") + if localEnv != "" { + file = fabfile.MustFind("config." + localEnv + ".toml") + } So(atom.LoadProviders(file, providers), ShouldBeNil) + So(os.Setenv("JOB_INLINE", "1"), ShouldBeNil) + t.Cleanup(func() { + _ = os.Unsetenv("JOB_INLINE") + }) + So(container.Container.Invoke(func(p struct { + dig.In + Initials []contracts.Initial `group:"initials"` + Job *job.Job + }) error { + _ = p.Initials + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + go func() { + _ = p.Job.Start(ctx) + }() + return nil + }), ShouldBeNil) So(container.Container.Invoke(invoke), ShouldBeNil) }) } diff --git a/backend/app/http/super/v1/routes.gen.go b/backend/app/http/super/v1/routes.gen.go index dd4a3b0..c1a6fb8 100644 --- a/backend/app/http/super/v1/routes.gen.go +++ b/backend/app/http/super/v1/routes.gen.go @@ -50,8 +50,8 @@ func (r *Routes) Register(router fiber.Router) { r.contents.List, Query[dto.SuperContentListFilter]("filter"), )) - r.log.Debugf("Registering route: Patch /super/v1/tenants/:tenantID/contents/:contentID/status -> contents.UpdateStatus") - router.Patch("/super/v1/tenants/:tenantID/contents/:contentID/status"[len(r.Path()):], Func3( + r.log.Debugf("Registering route: Patch /super/v1/tenants/:tenantID/contents/:contentID/status -> contents.UpdateStatus") + router.Patch("/super/v1/tenants/:tenantID/contents/:contentID/status"[len(r.Path()):], Func3( r.contents.UpdateStatus, PathParam[int64]("tenantID"), PathParam[int64]("contentID"), @@ -63,8 +63,8 @@ func (r *Routes) Register(router fiber.Router) { r.orders.List, Query[dto.SuperOrderListFilter]("filter"), )) - r.log.Debugf("Registering route: Get /super/v1/orders/:id -> orders.Get") - router.Get("/super/v1/orders/:id"[len(r.Path()):], DataFunc1( + r.log.Debugf("Registering route: Get /super/v1/orders/:id -> orders.Get") + router.Get("/super/v1/orders/:id"[len(r.Path()):], DataFunc1( r.orders.Get, PathParam[int64]("id"), )) @@ -72,8 +72,8 @@ func (r *Routes) Register(router fiber.Router) { router.Get("/super/v1/orders/statistics"[len(r.Path()):], DataFunc0( r.orders.Statistics, )) - r.log.Debugf("Registering route: Post /super/v1/orders/:id/refund -> orders.Refund") - router.Post("/super/v1/orders/:id/refund"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Post /super/v1/orders/:id/refund -> orders.Refund") + router.Post("/super/v1/orders/:id/refund"[len(r.Path()):], Func2( r.orders.Refund, PathParam[int64]("id"), Body[dto.SuperOrderRefundForm]("form"), @@ -84,8 +84,8 @@ func (r *Routes) Register(router fiber.Router) { r.tenants.List, Query[dto.TenantListFilter]("filter"), )) - r.log.Debugf("Registering route: Get /super/v1/tenants/:id -> tenants.Get") - router.Get("/super/v1/tenants/:id"[len(r.Path()):], DataFunc1( + r.log.Debugf("Registering route: Get /super/v1/tenants/:id -> tenants.Get") + router.Get("/super/v1/tenants/:id"[len(r.Path()):], DataFunc1( r.tenants.Get, PathParam[int64]("id"), )) @@ -93,14 +93,14 @@ func (r *Routes) Register(router fiber.Router) { router.Get("/super/v1/tenants/statuses"[len(r.Path()):], DataFunc0( r.tenants.Statuses, )) - r.log.Debugf("Registering route: Patch /super/v1/tenants/:id -> tenants.UpdateExpire") - router.Patch("/super/v1/tenants/:id"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Patch /super/v1/tenants/:id -> tenants.UpdateExpire") + router.Patch("/super/v1/tenants/:id"[len(r.Path()):], Func2( r.tenants.UpdateExpire, PathParam[int64]("id"), Body[dto.TenantExpireUpdateForm]("form"), )) - r.log.Debugf("Registering route: Patch /super/v1/tenants/:id/status -> tenants.UpdateStatus") - router.Patch("/super/v1/tenants/:id/status"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Patch /super/v1/tenants/:id/status -> tenants.UpdateStatus") + router.Patch("/super/v1/tenants/:id/status"[len(r.Path()):], Func2( r.tenants.UpdateStatus, PathParam[int64]("id"), Body[dto.TenantStatusUpdateForm]("form"), @@ -116,8 +116,8 @@ func (r *Routes) Register(router fiber.Router) { r.users.List, Query[dto.UserListFilter]("filter"), )) - r.log.Debugf("Registering route: Get /super/v1/users/:id -> users.Get") - router.Get("/super/v1/users/:id"[len(r.Path()):], DataFunc1( + r.log.Debugf("Registering route: Get /super/v1/users/:id -> users.Get") + router.Get("/super/v1/users/:id"[len(r.Path()):], DataFunc1( r.users.Get, PathParam[int64]("id"), )) @@ -129,14 +129,14 @@ func (r *Routes) Register(router fiber.Router) { router.Get("/super/v1/users/statuses"[len(r.Path()):], DataFunc0( r.users.Statuses, )) - r.log.Debugf("Registering route: Patch /super/v1/users/:id/roles -> users.UpdateRoles") - router.Patch("/super/v1/users/:id/roles"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Patch /super/v1/users/:id/roles -> users.UpdateRoles") + router.Patch("/super/v1/users/:id/roles"[len(r.Path()):], Func2( r.users.UpdateRoles, PathParam[int64]("id"), Body[dto.UserRolesUpdateForm]("form"), )) - r.log.Debugf("Registering route: Patch /super/v1/users/:id/status -> users.UpdateStatus") - router.Patch("/super/v1/users/:id/status"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Patch /super/v1/users/:id/status -> users.UpdateStatus") + router.Patch("/super/v1/users/:id/status"[len(r.Path()):], Func2( r.users.UpdateStatus, PathParam[int64]("id"), Body[dto.UserStatusUpdateForm]("form"), diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index 598e905..0a5aaac 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -50,11 +50,11 @@ func (r *Routes) Name() string { // Each route is registered with its corresponding controller action and parameter bindings. func (r *Routes) Register(router fiber.Router) { // Register routes for controller: Common - r.log.Debugf("Registering route: Delete /v1/media-assets/:id -> common.DeleteMediaAsset") - router.Delete("/v1/media-assets/:id"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Delete /v1/media-assets/:id -> common.DeleteMediaAsset") + router.Delete("/v1/media-assets/:id"[len(r.Path()):], Func2( r.common.DeleteMediaAsset, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) r.log.Debugf("Registering route: Delete /v1/upload/:uploadId -> common.AbortUpload") router.Delete("/v1/upload/:uploadId"[len(r.Path()):], Func2( @@ -99,69 +99,69 @@ func (r *Routes) Register(router fiber.Router) { Body[dto.UploadPartForm]("form"), )) // Register routes for controller: Content - r.log.Debugf("Registering route: Delete /v1/contents/:id/favorite -> content.RemoveFavorite") - router.Delete("/v1/contents/:id/favorite"[len(r.Path()):], Func1( + r.log.Debugf("Registering route: Delete /v1/contents/:id/favorite -> content.RemoveFavorite") + router.Delete("/v1/contents/:id/favorite"[len(r.Path()):], Func1( r.content.RemoveFavorite, - PathParam[string]("id"), + PathParam[int64]("id"), )) - r.log.Debugf("Registering route: Delete /v1/contents/:id/like -> content.RemoveLike") - router.Delete("/v1/contents/:id/like"[len(r.Path()):], Func1( + r.log.Debugf("Registering route: Delete /v1/contents/:id/like -> content.RemoveLike") + router.Delete("/v1/contents/:id/like"[len(r.Path()):], Func1( r.content.RemoveLike, - PathParam[string]("id"), + PathParam[int64]("id"), )) r.log.Debugf("Registering route: Get /v1/contents -> content.List") router.Get("/v1/contents"[len(r.Path()):], DataFunc1( r.content.List, Query[dto.ContentListFilter]("filter"), )) - r.log.Debugf("Registering route: Get /v1/contents/:id -> content.Get") - router.Get("/v1/contents/:id"[len(r.Path()):], DataFunc1( + r.log.Debugf("Registering route: Get /v1/contents/:id -> content.Get") + router.Get("/v1/contents/:id"[len(r.Path()):], DataFunc1( r.content.Get, - PathParam[string]("id"), + PathParam[int64]("id"), )) - r.log.Debugf("Registering route: Get /v1/contents/:id/comments -> content.ListComments") - router.Get("/v1/contents/:id/comments"[len(r.Path()):], DataFunc2( + r.log.Debugf("Registering route: Get /v1/contents/:id/comments -> content.ListComments") + router.Get("/v1/contents/:id/comments"[len(r.Path()):], DataFunc2( r.content.ListComments, - PathParam[string]("id"), + PathParam[int64]("id"), QueryParam[int]("page"), )) r.log.Debugf("Registering route: Get /v1/topics -> content.ListTopics") router.Get("/v1/topics"[len(r.Path()):], DataFunc0( r.content.ListTopics, )) - r.log.Debugf("Registering route: Post /v1/comments/:id/like -> content.LikeComment") - router.Post("/v1/comments/:id/like"[len(r.Path()):], Func1( + r.log.Debugf("Registering route: Post /v1/comments/:id/like -> content.LikeComment") + router.Post("/v1/comments/:id/like"[len(r.Path()):], Func1( r.content.LikeComment, - PathParam[string]("id"), + PathParam[int64]("id"), )) - r.log.Debugf("Registering route: Post /v1/contents/:id/comments -> content.CreateComment") - router.Post("/v1/contents/:id/comments"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Post /v1/contents/:id/comments -> content.CreateComment") + router.Post("/v1/contents/:id/comments"[len(r.Path()):], Func2( r.content.CreateComment, - PathParam[string]("id"), + PathParam[int64]("id"), Body[dto.CommentCreateForm]("form"), )) - r.log.Debugf("Registering route: Post /v1/contents/:id/favorite -> content.AddFavorite") - router.Post("/v1/contents/:id/favorite"[len(r.Path()):], Func1( + r.log.Debugf("Registering route: Post /v1/contents/:id/favorite -> content.AddFavorite") + router.Post("/v1/contents/:id/favorite"[len(r.Path()):], Func1( r.content.AddFavorite, - PathParam[string]("id"), + PathParam[int64]("id"), )) - r.log.Debugf("Registering route: Post /v1/contents/:id/like -> content.AddLike") - router.Post("/v1/contents/:id/like"[len(r.Path()):], Func1( + r.log.Debugf("Registering route: Post /v1/contents/:id/like -> content.AddLike") + router.Post("/v1/contents/:id/like"[len(r.Path()):], Func1( r.content.AddLike, - PathParam[string]("id"), + PathParam[int64]("id"), )) // Register routes for controller: Creator - r.log.Debugf("Registering route: Delete /v1/creator/contents/:id -> creator.DeleteContent") - router.Delete("/v1/creator/contents/:id"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Delete /v1/creator/contents/:id -> creator.DeleteContent") + router.Delete("/v1/creator/contents/:id"[len(r.Path()):], Func2( r.creator.DeleteContent, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) r.log.Debugf("Registering route: Delete /v1/creator/payout-accounts -> creator.RemovePayoutAccount") router.Delete("/v1/creator/payout-accounts"[len(r.Path()):], Func2( r.creator.RemovePayoutAccount, Local[*models.User]("__ctx_user"), - QueryParam[string]("id"), + QueryParam[int64]("id"), )) r.log.Debugf("Registering route: Get /v1/creator/contents -> creator.ListContents") router.Get("/v1/creator/contents"[len(r.Path()):], DataFunc2( @@ -169,11 +169,11 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("__ctx_user"), Query[dto.CreatorContentListFilter]("filter"), )) - r.log.Debugf("Registering route: Get /v1/creator/contents/:id -> creator.GetContent") - router.Get("/v1/creator/contents/:id"[len(r.Path()):], DataFunc2( + r.log.Debugf("Registering route: Get /v1/creator/contents/:id -> creator.GetContent") + router.Get("/v1/creator/contents/:id"[len(r.Path()):], DataFunc2( r.creator.GetContent, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) r.log.Debugf("Registering route: Get /v1/creator/dashboard -> creator.Dashboard") router.Get("/v1/creator/dashboard"[len(r.Path()):], DataFunc1( @@ -208,11 +208,11 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("__ctx_user"), Body[dto.ContentCreateForm]("form"), )) - r.log.Debugf("Registering route: Post /v1/creator/orders/:id/refund -> creator.Refund") - router.Post("/v1/creator/orders/:id/refund"[len(r.Path()):], Func3( + r.log.Debugf("Registering route: Post /v1/creator/orders/:id/refund -> creator.Refund") + router.Post("/v1/creator/orders/:id/refund"[len(r.Path()):], Func3( r.creator.Refund, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), Body[dto.RefundForm]("form"), )) r.log.Debugf("Registering route: Post /v1/creator/payout-accounts -> creator.AddPayoutAccount") @@ -227,11 +227,11 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("__ctx_user"), Body[dto.WithdrawForm]("form"), )) - r.log.Debugf("Registering route: Put /v1/creator/contents/:id -> creator.UpdateContent") - router.Put("/v1/creator/contents/:id"[len(r.Path()):], Func3( + r.log.Debugf("Registering route: Put /v1/creator/contents/:id -> creator.UpdateContent") + router.Put("/v1/creator/contents/:id"[len(r.Path()):], Func3( r.creator.UpdateContent, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), Body[dto.ContentUpdateForm]("form"), )) r.log.Debugf("Registering route: Put /v1/creator/settings -> creator.UpdateSettings") @@ -254,16 +254,16 @@ func (r *Routes) Register(router fiber.Router) { QueryParam[string]("sign"), )) // Register routes for controller: Tenant - r.log.Debugf("Registering route: Delete /v1/tenants/:id/follow -> tenant.Unfollow") - router.Delete("/v1/tenants/:id/follow"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Delete /v1/tenants/:id/follow -> tenant.Unfollow") + router.Delete("/v1/tenants/:id/follow"[len(r.Path()):], Func2( r.tenant.Unfollow, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) - r.log.Debugf("Registering route: Get /v1/creators/:id/contents -> tenant.ListContents") - router.Get("/v1/creators/:id/contents"[len(r.Path()):], DataFunc2( + r.log.Debugf("Registering route: Get /v1/creators/:id/contents -> tenant.ListContents") + router.Get("/v1/creators/:id/contents"[len(r.Path()):], DataFunc2( r.tenant.ListContents, - PathParam[string]("id"), + PathParam[int64]("id"), Query[dto.ContentListFilter]("filter"), )) r.log.Debugf("Registering route: Get /v1/tenants -> tenant.List") @@ -271,23 +271,23 @@ func (r *Routes) Register(router fiber.Router) { r.tenant.List, Query[dto.TenantListFilter]("filter"), )) - r.log.Debugf("Registering route: Get /v1/tenants/:id -> tenant.Get") - router.Get("/v1/tenants/:id"[len(r.Path()):], DataFunc2( + r.log.Debugf("Registering route: Get /v1/tenants/:id -> tenant.Get") + router.Get("/v1/tenants/:id"[len(r.Path()):], DataFunc2( r.tenant.Get, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) - r.log.Debugf("Registering route: Post /v1/tenants/:id/follow -> tenant.Follow") - router.Post("/v1/tenants/:id/follow"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Post /v1/tenants/:id/follow -> tenant.Follow") + router.Post("/v1/tenants/:id/follow"[len(r.Path()):], Func2( r.tenant.Follow, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) // Register routes for controller: Transaction - r.log.Debugf("Registering route: Get /v1/orders/:id/status -> transaction.Status") - router.Get("/v1/orders/:id/status"[len(r.Path()):], DataFunc1( + r.log.Debugf("Registering route: Get /v1/orders/:id/status -> transaction.Status") + router.Get("/v1/orders/:id/status"[len(r.Path()):], DataFunc1( r.transaction.Status, - PathParam[string]("id"), + PathParam[int64]("id"), )) r.log.Debugf("Registering route: Post /v1/orders -> transaction.Create") router.Post("/v1/orders"[len(r.Path()):], DataFunc2( @@ -295,11 +295,11 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("__ctx_user"), Body[dto.OrderCreateForm]("form"), )) - r.log.Debugf("Registering route: Post /v1/orders/:id/pay -> transaction.Pay") - router.Post("/v1/orders/:id/pay"[len(r.Path()):], DataFunc3( + r.log.Debugf("Registering route: Post /v1/orders/:id/pay -> transaction.Pay") + router.Post("/v1/orders/:id/pay"[len(r.Path()):], DataFunc3( r.transaction.Pay, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), Body[dto.OrderPayForm]("form"), )) r.log.Debugf("Registering route: Post /v1/webhook/payment/notify -> transaction.Webhook") @@ -308,17 +308,17 @@ func (r *Routes) Register(router fiber.Router) { Body[WebhookForm]("form"), )) // Register routes for controller: User - r.log.Debugf("Registering route: Delete /v1/me/favorites/:contentId -> user.RemoveFavorite") - router.Delete("/v1/me/favorites/:contentId"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Delete /v1/me/favorites/:contentId -> user.RemoveFavorite") + router.Delete("/v1/me/favorites/:contentId"[len(r.Path()):], Func2( r.user.RemoveFavorite, Local[*models.User]("__ctx_user"), - PathParam[string]("contentId"), + PathParam[int64]("contentId"), )) - r.log.Debugf("Registering route: Delete /v1/me/likes/:contentId -> user.RemoveLike") - router.Delete("/v1/me/likes/:contentId"[len(r.Path()):], Func2( + r.log.Debugf("Registering route: Delete /v1/me/likes/:contentId -> user.RemoveLike") + router.Delete("/v1/me/likes/:contentId"[len(r.Path()):], Func2( r.user.RemoveLike, Local[*models.User]("__ctx_user"), - PathParam[string]("contentId"), + PathParam[int64]("contentId"), )) r.log.Debugf("Registering route: Get /v1/me -> user.Me") router.Get("/v1/me"[len(r.Path()):], DataFunc1( @@ -351,17 +351,6 @@ func (r *Routes) Register(router fiber.Router) { r.user.Likes, Local[*models.User]("__ctx_user"), )) - r.log.Debugf("Registering route: Post /v1/me/notifications/:id/read -> user.MarkNotificationRead") - router.Post("/v1/me/notifications/:id/read"[len(r.Path()):], Func2( - r.user.MarkNotificationRead, - Local[*models.User]("__ctx_user"), - PathParam[string]("id"), - )) - r.log.Debugf("Registering route: Post /v1/me/notifications/read-all -> user.MarkAllNotificationsRead") - router.Post("/v1/me/notifications/read-all"[len(r.Path()):], Func1( - r.user.MarkAllNotificationsRead, - Local[*models.User]("__ctx_user"), - )) r.log.Debugf("Registering route: Get /v1/me/notifications -> user.Notifications") router.Get("/v1/me/notifications"[len(r.Path()):], DataFunc3( r.user.Notifications, @@ -375,11 +364,11 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("__ctx_user"), QueryParam[string]("status"), )) - r.log.Debugf("Registering route: Get /v1/me/orders/:id -> user.GetOrder") - router.Get("/v1/me/orders/:id"[len(r.Path()):], DataFunc2( + r.log.Debugf("Registering route: Get /v1/me/orders/:id -> user.GetOrder") + router.Get("/v1/me/orders/:id"[len(r.Path()):], DataFunc2( r.user.GetOrder, Local[*models.User]("__ctx_user"), - PathParam[string]("id"), + PathParam[int64]("id"), )) r.log.Debugf("Registering route: Get /v1/me/wallet -> user.Wallet") router.Get("/v1/me/wallet"[len(r.Path()):], DataFunc1( @@ -390,13 +379,24 @@ func (r *Routes) Register(router fiber.Router) { router.Post("/v1/me/favorites"[len(r.Path()):], Func2( r.user.AddFavorite, Local[*models.User]("__ctx_user"), - QueryParam[string]("contentId"), + QueryParam[int64]("contentId"), )) r.log.Debugf("Registering route: Post /v1/me/likes -> user.AddLike") router.Post("/v1/me/likes"[len(r.Path()):], Func2( r.user.AddLike, Local[*models.User]("__ctx_user"), - QueryParam[string]("contentId"), + QueryParam[int64]("contentId"), + )) + r.log.Debugf("Registering route: Post /v1/me/notifications/:id/read -> user.MarkNotificationRead") + router.Post("/v1/me/notifications/:id/read"[len(r.Path()):], Func2( + r.user.MarkNotificationRead, + Local[*models.User]("__ctx_user"), + PathParam[int64]("id"), + )) + r.log.Debugf("Registering route: Post /v1/me/notifications/read-all -> user.MarkAllNotificationsRead") + router.Post("/v1/me/notifications/read-all"[len(r.Path()):], Func1( + r.user.MarkAllNotificationsRead, + Local[*models.User]("__ctx_user"), )) r.log.Debugf("Registering route: Post /v1/me/realname -> user.RealName") router.Post("/v1/me/realname"[len(r.Path()):], Func2( diff --git a/backend/app/services/common.go b/backend/app/services/common.go index cffe1ae..f0f5cec 100644 --- a/backend/app/services/common.go +++ b/backend/app/services/common.go @@ -260,7 +260,7 @@ func (s *common) CompleteUpload(ctx context.Context, userID int64, form *common_ return s.composeUploadResult(asset), nil } -func (s *common) DeleteMediaAsset(ctx context.Context, userID int64, id int64) error { +func (s *common) DeleteMediaAsset(ctx context.Context, userID, id int64) error { asset, err := models.MediaAssetQuery.WithContext(ctx). Where(models.MediaAssetQuery.ID.Eq(id), models.MediaAssetQuery.UserID.Eq(userID)). First() diff --git a/backend/app/services/content.go b/backend/app/services/content.go index a02c134..b7c5314 100644 --- a/backend/app/services/content.go +++ b/backend/app/services/content.go @@ -11,6 +11,7 @@ import ( "quyun/v2/database/models" "quyun/v2/pkg/consts" + "go.ipao.vip/gen/types" "gorm.io/gorm" ) @@ -127,8 +128,7 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte }, nil } -func (s *content) Get(ctx context.Context, userID int64, id int64) (*content_dto.ContentDetail, error) { - +func (s *content) Get(ctx context.Context, userID, id int64) (*content_dto.ContentDetail, error) { // Increment Views _, _ = models.ContentQuery.WithContext(ctx). Where(models.ContentQuery.ID.Eq(id)). @@ -212,7 +212,7 @@ func (s *content) Get(ctx context.Context, userID int64, id int64) (*content_dto if userID > 0 { exists, _ := models.TenantUserQuery.WithContext(ctx). Where(models.TenantUserQuery.TenantID.Eq(item.TenantID), - models.TenantUserQuery.Role.Contains(string(consts.TenantUserRoleMember))). + models.TenantUserQuery.Role.Contains(types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember})). Exists() authorIsFollowing = exists } @@ -232,7 +232,7 @@ func (s *content) Get(ctx context.Context, userID int64, id int64) (*content_dto return detail, nil } -func (s *content) ListComments(ctx context.Context, userID int64, id int64, page int) (*requests.Pager, error) { +func (s *content) ListComments(ctx context.Context, userID, id int64, page int) (*requests.Pager, error) { tbl, q := models.CommentQuery.QueryContext(ctx) q = q.Where(tbl.ContentID.Eq(id)).Preload(tbl.User) @@ -319,7 +319,7 @@ func (s *content) CreateComment( return nil } -func (s *content) LikeComment(ctx context.Context, userID int64, id int64) error { +func (s *content) LikeComment(ctx context.Context, userID, id int64) error { if userID == 0 { return errorx.ErrUnauthorized } @@ -402,11 +402,11 @@ func (s *content) GetFavorites(ctx context.Context, userID int64) ([]user_dto.Co return s.getInteractList(ctx, userID, "favorite") } -func (s *content) AddFavorite(ctx context.Context, userID int64, contentId int64) error { +func (s *content) AddFavorite(ctx context.Context, userID, contentId int64) error { return s.addInteract(ctx, userID, contentId, "favorite") } -func (s *content) RemoveFavorite(ctx context.Context, userID int64, contentId int64) error { +func (s *content) RemoveFavorite(ctx context.Context, userID, contentId int64) error { return s.removeInteract(ctx, userID, contentId, "favorite") } @@ -414,11 +414,11 @@ func (s *content) GetLikes(ctx context.Context, userID int64) ([]user_dto.Conten return s.getInteractList(ctx, userID, "like") } -func (s *content) AddLike(ctx context.Context, userID int64, contentId int64) error { +func (s *content) AddLike(ctx context.Context, userID, contentId int64) error { return s.addInteract(ctx, userID, contentId, "like") } -func (s *content) RemoveLike(ctx context.Context, userID int64, contentId int64) error { +func (s *content) RemoveLike(ctx context.Context, userID, contentId int64) error { return s.removeInteract(ctx, userID, contentId, "like") } @@ -554,7 +554,7 @@ func (s *content) toMediaURLs(assets []*models.ContentAsset) []content_dto.Media return urls } -func (s *content) addInteract(ctx context.Context, userID int64, contentId int64, typ string) error { +func (s *content) addInteract(ctx context.Context, userID, contentId int64, typ string) error { if userID == 0 { return errorx.ErrUnauthorized } @@ -605,7 +605,7 @@ func (s *content) addInteract(ctx context.Context, userID int64, contentId int64 return nil } -func (s *content) removeInteract(ctx context.Context, userID int64, contentId int64, typ string) error { +func (s *content) removeInteract(ctx context.Context, userID, contentId int64, typ string) error { if userID == 0 { return errorx.ErrUnauthorized } diff --git a/backend/app/services/content_test.go b/backend/app/services/content_test.go index f9264d9..db0f7bc 100644 --- a/backend/app/services/content_test.go +++ b/backend/app/services/content_test.go @@ -65,7 +65,7 @@ func (s *ContentTestSuite) Test_List() { models.ContentQuery.WithContext(ctx).Create(c1, c2) Convey("should list only published contents", func() { - tid := "1" + tid := int64(1) filter := &content_dto.ContentListFilter{ TenantID: &tid, Pagination: requests.Pagination{ @@ -130,7 +130,7 @@ func (s *ContentTestSuite) Test_Get() { So(detail.Title, ShouldEqual, "Detail Content") So(detail.AuthorName, ShouldEqual, "Author1") So(len(detail.MediaUrls), ShouldEqual, 1) - So(detail.MediaUrls[0].URL, ShouldEndWith, "test.mp4") + So(detail.MediaUrls[0].URL, ShouldContainSubstring, "test.mp4") }) }) } @@ -197,7 +197,7 @@ func (s *ContentTestSuite) Test_Library() { So(len(list), ShouldEqual, 1) So(list[0].Title, ShouldEqual, "Paid Content") So(list[0].Type, ShouldEqual, "video") - So(list[0].Cover, ShouldEndWith, "cover.jpg") + So(list[0].Cover, ShouldContainSubstring, "cover.jpg") So(list[0].IsPurchased, ShouldBeTrue) }) }) @@ -327,7 +327,7 @@ func (s *ContentTestSuite) Test_PreviewLogic() { detail, err := Content.Get(guestCtx, 0, c.ID) So(err, ShouldBeNil) So(len(detail.MediaUrls), ShouldEqual, 1) - So(detail.MediaUrls[0].URL, ShouldEndWith, "preview.mp4") + So(detail.MediaUrls[0].URL, ShouldContainSubstring, "preview.mp4") So(detail.IsPurchased, ShouldBeFalse) }) diff --git a/backend/app/services/coupon_test.go b/backend/app/services/coupon_test.go index 2b036f9..955d1a0 100644 --- a/backend/app/services/coupon_test.go +++ b/backend/app/services/coupon_test.go @@ -9,7 +9,6 @@ import ( "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" - "quyun/v2/providers/storage" . "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/suite" @@ -30,7 +29,7 @@ type CouponTestSuite struct { } func Test_Coupon(t *testing.T) { - providers := testx.Default().With(Provide).With(storage.Provide) + providers := testx.Default().With(Provide) testx.Serve(providers, t, func(p CouponTestSuiteInjectParams) { suite.Run(t, &CouponTestSuite{CouponTestSuiteInjectParams: p}) diff --git a/backend/app/services/creator.go b/backend/app/services/creator.go index 4bb0ea4..c006ffd 100644 --- a/backend/app/services/creator.go +++ b/backend/app/services/creator.go @@ -451,7 +451,7 @@ func (s *creator) UpdateContent( }) } -func (s *creator) DeleteContent(ctx context.Context, userID int64, id int64) error { +func (s *creator) DeleteContent(ctx context.Context, userID, id int64) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err @@ -472,7 +472,7 @@ func (s *creator) DeleteContent(ctx context.Context, userID int64, id int64) err return nil } -func (s *creator) GetContent(ctx context.Context, userID int64, id int64) (*creator_dto.ContentEditDTO, error) { +func (s *creator) GetContent(ctx context.Context, userID, id int64) (*creator_dto.ContentEditDTO, error) { tid, err := s.getTenantID(ctx, userID) if err != nil { return nil, err @@ -634,7 +634,7 @@ func (s *creator) ListOrders( return data, nil } -func (s *creator) ProcessRefund(ctx context.Context, userID int64, id int64, form *creator_dto.RefundForm) error { +func (s *creator) ProcessRefund(ctx context.Context, userID, id int64, form *creator_dto.RefundForm) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err @@ -699,7 +699,7 @@ func (s *creator) ProcessRefund(ctx context.Context, userID int64, id int64, for // 4. Revoke Content Access // Fetch order items to get content IDs - items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(oid)).Find() + items, _ := tx.OrderItem.WithContext(ctx).Where(tx.OrderItem.OrderID.Eq(o.ID)).Find() contentIDs := make([]int64, len(items)) for i, item := range items { contentIDs[i] = item.ContentID @@ -717,7 +717,7 @@ func (s *creator) ProcessRefund(ctx context.Context, userID int64, id int64, for ledger := &models.TenantLedger{ TenantID: tid, UserID: uid, - OrderID: oid, + OrderID: o.ID, Type: consts.TenantLedgerTypeCreditRefund, Amount: o.AmountPaid, Remark: "退款: " + form.Reason, @@ -824,7 +824,7 @@ func (s *creator) AddPayoutAccount(ctx context.Context, userID int64, form *crea return nil } -func (s *creator) RemovePayoutAccount(ctx context.Context, userID int64, id int64) error { +func (s *creator) RemovePayoutAccount(ctx context.Context, userID, id int64) error { tid, err := s.getTenantID(ctx, userID) if err != nil { return err diff --git a/backend/app/services/creator_test.go b/backend/app/services/creator_test.go index 1f49850..b82d938 100644 --- a/backend/app/services/creator_test.go +++ b/backend/app/services/creator_test.go @@ -139,10 +139,11 @@ func (s *CreatorTestSuite) Test_UpdateContent() { Create(&models.ContentPrice{TenantID: t.ID, UserID: u.ID, ContentID: c.ID, PriceAmount: 100}) Convey("should update content", func() { + price := 20.00 form := &creator_dto.ContentUpdateForm{ Title: "New Title", Genre: "video", - Price: 20.00, + Price: &price, } err := Creator.UpdateContent(ctx, u.ID, c.ID, form) So(err, ShouldBeNil) @@ -260,7 +261,7 @@ func (s *CreatorTestSuite) Test_Withdraw() { models.TableNameTenantLedger, ) - u := &models.User{Username: "creator6", Phone: "13700000006", Balance: 5000} // 50.00 + u := &models.User{Username: "creator6", Phone: "13700000006", Balance: 5000, IsRealNameVerified: true} // 50.00 models.UserQuery.WithContext(ctx).Create(u) ctx = context.WithValue(ctx, consts.CtxKeyUser, u.ID) diff --git a/backend/app/services/notification.go b/backend/app/services/notification.go index 73a432c..01541f9 100644 --- a/backend/app/services/notification.go +++ b/backend/app/services/notification.go @@ -2,6 +2,7 @@ package services import ( "context" + "os" "time" "quyun/v2/app/errorx" @@ -57,7 +58,7 @@ func (s *notification) List(ctx context.Context, userID int64, page int, typeArg }, nil } -func (s *notification) MarkRead(ctx context.Context, userID int64, id int64) error { +func (s *notification) MarkRead(ctx context.Context, userID, id int64) error { _, err := models.NotificationQuery.WithContext(ctx). Where(models.NotificationQuery.ID.Eq(id), models.NotificationQuery.UserID.Eq(userID)). UpdateSimple(models.NotificationQuery.IsRead.Value(true)) @@ -84,5 +85,19 @@ func (s *notification) Send(ctx context.Context, userID int64, typ, title, conte Title: title, Content: content, } + // 测试环境下同步写入,避免异步任务未启动导致结果不确定。 + if os.Getenv("JOB_INLINE") == "1" { + n := &models.Notification{ + UserID: userID, + Type: typ, + Title: title, + Content: content, + IsRead: false, + } + if err := models.NotificationQuery.WithContext(ctx).Create(n); err != nil { + return errorx.ErrDatabaseError.WithCause(err) + } + return nil + } return s.job.Add(arg) } diff --git a/backend/app/services/notification_test.go b/backend/app/services/notification_test.go index 0a86233..95b7d37 100644 --- a/backend/app/services/notification_test.go +++ b/backend/app/services/notification_test.go @@ -4,9 +4,11 @@ import ( "context" "database/sql" "testing" + "time" "quyun/v2/app/commands/testx" app_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/app/requests" "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" @@ -49,8 +51,15 @@ func (s *NotificationTestSuite) Test_CRUD() { err := Notification.Send(ctx, uID, "system", "Welcome", "Hello World") So(err, ShouldBeNil) - list, err := Notification.List(ctx, uID, 1, "") - So(err, ShouldBeNil) + var list *requests.Pager + for i := 0; i < 5; i++ { + list, err = Notification.List(ctx, uID, 1, "") + So(err, ShouldBeNil) + if list.Total > 0 { + break + } + time.Sleep(50 * time.Millisecond) + } So(list.Total, ShouldEqual, 1) items := list.Items.([]app_dto.Notification) diff --git a/backend/app/services/order.go b/backend/app/services/order.go index 71498a1..b6dd150 100644 --- a/backend/app/services/order.go +++ b/backend/app/services/order.go @@ -46,7 +46,7 @@ func (s *order) ListUserOrders(ctx context.Context, userID int64, status string) return data, nil } -func (s *order) GetUserOrder(ctx context.Context, userID int64, id int64) (*user_dto.Order, error) { +func (s *order) GetUserOrder(ctx context.Context, userID, id int64) (*user_dto.Order, error) { if userID == 0 { return nil, errorx.ErrUnauthorized } diff --git a/backend/app/services/order_test.go b/backend/app/services/order_test.go index 33fe8da..d01adfb 100644 --- a/backend/app/services/order_test.go +++ b/backend/app/services/order_test.go @@ -210,7 +210,7 @@ func (s *OrderTestSuite) Test_OrderDetails() { So(detail.TenantName, ShouldEqual, "Best Shop") So(len(detail.Items), ShouldEqual, 1) So(detail.Items[0].Title, ShouldEqual, "Amazing Song") - So(detail.Items[0].Cover, ShouldEndWith, "cover.jpg") + So(detail.Items[0].Cover, ShouldContainSubstring, "cover.jpg") So(detail.Amount, ShouldEqual, 5.00) }) }) diff --git a/backend/app/services/provider.gen.go b/backend/app/services/provider.gen.go index 5874d9c..f537319 100755 --- a/backend/app/services/provider.gen.go +++ b/backend/app/services/provider.gen.go @@ -3,6 +3,7 @@ package services import ( "quyun/v2/providers/job" "quyun/v2/providers/jwt" + jwt_provider "quyun/v2/providers/jwt" "quyun/v2/providers/storage" "go.ipao.vip/atom" @@ -106,8 +107,12 @@ func Provide(opts ...opt.Option) error { }, atom.GroupInitial); err != nil { return err } - if err := container.Container.Provide(func() (*super, error) { - obj := &super{} + if err := container.Container.Provide(func( + jwt *jwt_provider.JWT, + ) (*super, error) { + obj := &super{ + jwt: jwt, + } return obj, nil }); err != nil { diff --git a/backend/app/services/tenant.go b/backend/app/services/tenant.go index e3016d5..a3cc6ed 100644 --- a/backend/app/services/tenant.go +++ b/backend/app/services/tenant.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "quyun/v2/app/errorx" "quyun/v2/app/http/v1/dto" "quyun/v2/app/requests" @@ -62,7 +63,7 @@ func (s *tenant) List(ctx context.Context, filter *dto.TenantListFilter) (*reque }, nil } -func (s *tenant) GetPublicProfile(ctx context.Context, userID int64, id int64) (*dto.TenantProfile, error) { +func (s *tenant) GetPublicProfile(ctx context.Context, userID, id int64) (*dto.TenantProfile, error) { t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(id)).First() if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -102,7 +103,7 @@ func (s *tenant) GetPublicProfile(ctx context.Context, userID int64, id int64) ( }, nil } -func (s *tenant) Follow(ctx context.Context, userID int64, id int64) error { +func (s *tenant) Follow(ctx context.Context, userID, id int64) error { if userID == 0 { return errorx.ErrUnauthorized } @@ -131,7 +132,7 @@ func (s *tenant) Follow(ctx context.Context, userID int64, id int64) error { return nil } -func (s *tenant) Unfollow(ctx context.Context, userID int64, id int64) error { +func (s *tenant) Unfollow(ctx context.Context, userID, id int64) error { if userID == 0 { return errorx.ErrUnauthorized } diff --git a/backend/app/services/user_test.go b/backend/app/services/user_test.go index c8690e3..f45172e 100644 --- a/backend/app/services/user_test.go +++ b/backend/app/services/user_test.go @@ -44,7 +44,7 @@ func (s *UserTestSuite) Test_LoginWithOTP() { Convey("should create user and login success with correct OTP", func() { phone := "13800138000" - resp, err := User.LoginWithOTP(ctx, phone, "123456") + resp, err := User.LoginWithOTP(ctx, phone, "1234") So(err, ShouldBeNil) So(resp, ShouldNotBeNil) So(resp.Token, ShouldNotBeEmpty) @@ -55,11 +55,11 @@ func (s *UserTestSuite) Test_LoginWithOTP() { Convey("should login existing user", func() { phone := "13800138001" // Pre-create user - _, err := User.LoginWithOTP(ctx, phone, "123456") + _, err := User.LoginWithOTP(ctx, phone, "1234") So(err, ShouldBeNil) // Login again - resp, err := User.LoginWithOTP(ctx, phone, "123456") + resp, err := User.LoginWithOTP(ctx, phone, "1234") So(err, ShouldBeNil) So(resp.User.Phone, ShouldEqual, phone) }) @@ -79,7 +79,7 @@ func (s *UserTestSuite) Test_Me() { // Create user phone := "13800138003" - resp, _ := User.LoginWithOTP(ctx, phone, "123456") + resp, _ := User.LoginWithOTP(ctx, phone, "1234") userID := resp.User.ID Convey("should return user profile", func() { @@ -107,7 +107,7 @@ func (s *UserTestSuite) Test_Update() { database.Truncate(ctx, s.DB, models.TableNameUser) phone := "13800138004" - resp, _ := User.LoginWithOTP(ctx, phone, "123456") + resp, _ := User.LoginWithOTP(ctx, phone, "1234") userID := resp.User.ID ctx = context.WithValue(ctx, consts.CtxKeyUser, userID) @@ -135,7 +135,7 @@ func (s *UserTestSuite) Test_RealName() { database.Truncate(ctx, s.DB, models.TableNameUser) phone := "13800138005" - resp, _ := User.LoginWithOTP(ctx, phone, "123456") + resp, _ := User.LoginWithOTP(ctx, phone, "1234") userID := resp.User.ID ctx = context.WithValue(ctx, consts.CtxKeyUser, userID) @@ -160,7 +160,7 @@ func (s *UserTestSuite) Test_GetNotifications() { database.Truncate(ctx, s.DB, models.TableNameUser, models.TableNameNotification) phone := "13800138006" - resp, _ := User.LoginWithOTP(ctx, phone, "123456") + resp, _ := User.LoginWithOTP(ctx, phone, "1234") userID := resp.User.ID ctx = context.WithValue(ctx, consts.CtxKeyUser, userID) diff --git a/backend/app/services/wallet.go b/backend/app/services/wallet.go index 3e6cf1b..bdcb3ba 100644 --- a/backend/app/services/wallet.go +++ b/backend/app/services/wallet.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "strconv" "time" "quyun/v2/app/errorx" @@ -104,6 +105,6 @@ func (s *wallet) Recharge( // Mock Pay Params return &user_dto.RechargeResponse{ PayParams: "mock_paid_success", - OrderID: order.ID, + OrderID: strconv.FormatInt(order.ID, 10), }, nil } diff --git a/backend/config.test.toml b/backend/config.test.toml new file mode 100644 index 0000000..471f725 --- /dev/null +++ b/backend/config.test.toml @@ -0,0 +1,62 @@ +# Test configuration for local development. +# This file is used when ENV_LOCAL=test. + +[App] +Mode = "testing" +BaseURI = "http://localhost:8080" + +[App.Super] +Token = "" + +[Http] +Port = 8080 + +[Http.Cors] +Mode = "dev" + +[[Http.Cors.Whitelist]] +AllowOrigin = "http://localhost:5173" +AllowHeaders = "Content-Type,Authorization" +AllowMethods = "GET,POST,PUT,PATCH,DELETE,OPTIONS" +ExposeHeaders = "*" +AllowCredentials = true + +[Database] +Host = "127.0.0.1" +Port = 5432 +Database = "quyun_v2_test" +Username = "postgres" +Password = "postgres" +SslMode = "disable" +TimeZone = "Asia/Shanghai" +MaxIdleConns = 10 +MaxOpenConns = 20 +ConnMaxLifetime = "1800s" +ConnMaxIdleTime = "300s" + +[JWT] +SigningKey = "test-secret" +ExpiresTime = "168h" +Issuer = "v2" + +[HashIDs] +Salt = "test-salt" +MinLength = 8 + +[Redis] +Host = "127.0.0.1" +Port = 6379 +Password = "" +DB = 0 +PoolSize = 20 +MinIdleConns = 5 +MaxRetries = 3 +DialTimeout = "5s" +ReadTimeout = "3s" +WriteTimeout = "3s" + +[Storage] +Type = "local" +LocalPath = "./storage" +Secret = "test-storage-secret" +BaseURL = "/v1/storage"