优化结构
This commit is contained in:
parent
64459574b2
commit
4e5071ee65
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"kra/internal/initialize"
|
"kra/internal/initialize"
|
||||||
"kra/internal/integration"
|
"kra/internal/integration"
|
||||||
"kra/internal/integration/cache"
|
"kra/internal/integration/cache"
|
||||||
|
"kra/internal/modules"
|
||||||
"kra/internal/server"
|
"kra/internal/server"
|
||||||
"kra/internal/server/handler"
|
"kra/internal/server/handler"
|
||||||
"kra/internal/server/middleware"
|
"kra/internal/server/middleware"
|
||||||
|
|
@ -36,7 +37,7 @@ func wireApp(*conf.Server, *conf.Runtime, *slog.Logger, *logging.ReloadableLogge
|
||||||
handler.ProviderSet,
|
handler.ProviderSet,
|
||||||
router.ProviderSet,
|
router.ProviderSet,
|
||||||
worker.ProviderSet,
|
worker.ProviderSet,
|
||||||
app.Catalog,
|
modules.Catalog,
|
||||||
app.TaskRegistry,
|
app.TaskRegistry,
|
||||||
runtimeContributions,
|
runtimeContributions,
|
||||||
app.Runtime,
|
app.Runtime,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"kra/internal/integration/mq"
|
"kra/internal/integration/mq"
|
||||||
"kra/internal/integration/storage"
|
"kra/internal/integration/storage"
|
||||||
"kra/internal/integration/websocket"
|
"kra/internal/integration/websocket"
|
||||||
|
"kra/internal/modules"
|
||||||
"kra/internal/server"
|
"kra/internal/server"
|
||||||
"kra/internal/server/handler"
|
"kra/internal/server/handler"
|
||||||
"kra/internal/server/router"
|
"kra/internal/server/router"
|
||||||
|
|
@ -47,7 +48,7 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
catalog := app.Catalog()
|
catalog := modules.Catalog()
|
||||||
dataData, cleanup, err := data.NewData(runtime, logger, reloadable, catalog)
|
dataData, cleanup, err := data.NewData(runtime, logger, reloadable, catalog)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|
@ -100,7 +101,8 @@ func wireApp(confServer *conf.Server, runtime *conf.Runtime, logger *slog.Logger
|
||||||
emailUsecase := system2.NewEmailUsecase(emailRepo)
|
emailUsecase := system2.NewEmailUsecase(emailRepo)
|
||||||
emailService := service.NewEmailService(emailUsecase)
|
emailService := service.NewEmailService(emailUsecase)
|
||||||
handlerEmail := handler.NewEmail(emailService)
|
handlerEmail := handler.NewEmail(emailService)
|
||||||
paymentRepo := payment.NewPaymentRepo(dataData)
|
paymentConfigReader := integration.NewPaymentConfigReader(dataData)
|
||||||
|
paymentRepo := payment.NewPaymentRepo(dataData, paymentConfigReader)
|
||||||
paymentOrderRepo := payment.NewPaymentOrderRepo(dataData)
|
paymentOrderRepo := payment.NewPaymentOrderRepo(dataData)
|
||||||
paymentUsecase := payment2.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
|
paymentUsecase := payment2.NewPaymentUsecase(paymentRepo, paymentOrderRepo, logger)
|
||||||
paymentService := service.NewPaymentService(paymentUsecase)
|
paymentService := service.NewPaymentService(paymentUsecase)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
|
|
||||||
## system 内部保留边界
|
## system 内部保留边界
|
||||||
|
|
||||||
- `app`:组合根,汇总各模块的迁移、菜单、路由和任务贡献。
|
- `app`:运行时组合根,汇总依赖注入后的路由和任务贡献。
|
||||||
|
- `modules`:静态模块 catalog,汇总各模块迁移、菜单、API 和默认任务。
|
||||||
- `modules/system`:system 模块的 Definition,声明系统表迁移。
|
- `modules/system`:system 模块的 Definition,声明系统表迁移。
|
||||||
- `modules/integration`:integration 配置迁移和管理面贡献。
|
- `modules/integration`:integration 配置迁移和管理面贡献。
|
||||||
- `modules/task`:定时任务迁移和默认任务贡献。
|
- `modules/task`:定时任务迁移和默认任务贡献。
|
||||||
|
|
@ -44,8 +45,8 @@
|
||||||
|
|
||||||
```text
|
```text
|
||||||
internal/
|
internal/
|
||||||
app/ # 组合根和 catalog
|
app/ # 运行时组合根
|
||||||
modules/ # 业务模块定义及其模块级贡献
|
modules/ # 静态 catalog、业务模块定义及其模块级贡献
|
||||||
biz/
|
biz/
|
||||||
system/ # 系统领域
|
system/ # 系统领域
|
||||||
payment/ # 支付领域
|
payment/ # 支付领域
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
|
|
||||||
```text
|
```text
|
||||||
internal/
|
internal/
|
||||||
app/ # 组合根、模块 catalog 和运行时组合
|
app/ # 运行时组合根
|
||||||
modules/system/ # system 模块定义
|
modules/ # 静态 catalog 和模块定义
|
||||||
modules/payment/ # payment 模块定义
|
modules/payment/ # payment 模块定义
|
||||||
biz/ # DO、usecase、repo interface
|
biz/ # DO、usecase、repo interface
|
||||||
conf/ # 配置 proto/runtime
|
conf/ # 配置 proto/runtime
|
||||||
|
|
@ -40,12 +40,12 @@ internal/
|
||||||
独立边界时不继续拆分。
|
独立边界时不继续拆分。
|
||||||
- 删除只转发 `pkg/protoutil` 的 `utils/configutil`。
|
- 删除只转发 `pkg/protoutil` 的 `utils/configutil`。
|
||||||
|
|
||||||
## `internal/app` 为什么只保留组合代码
|
## `internal/app` 为什么只保留运行时组合
|
||||||
|
|
||||||
`app/catalog.go` 是有意保留的组合根,负责组装模块、任务注册与运行时。
|
`modules/catalog.go` 是静态模块注册点,负责按依赖顺序汇总各模块
|
||||||
system 自身的迁移、管理面和默认定时任务位于
|
`Definition()`;`app/runtime.go` 只负责任务注册和依赖注入后的运行时路由组合。
|
||||||
`modules/system/definition.go`,由模块包声明后再被 catalog 汇总。这样模块
|
这样模块定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或
|
||||||
定义不再和应用组合逻辑混在一起,也不能误并入 `biz`、`service` 或 `data`。
|
`data`。
|
||||||
|
|
||||||
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
|
Catalog 只能自动汇总静态模块贡献;新增模块若提供运行时路由或依赖型任务,仍需
|
||||||
在 cmd/Wire 中显式注册,直到统一的 runtime contribution 协议落地。
|
在 cmd/Wire 中显式注册,直到统一的 runtime contribution 协议落地。
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
系统模块承载当前管理后台的完整业务边界。`internal` 顶层只保留有明确
|
系统模块承载当前管理后台的完整业务边界。`internal` 顶层只保留有明确
|
||||||
生命周期或分层职责的包:
|
生命周期或分层职责的包:
|
||||||
|
|
||||||
- `app`:组合根、模块 catalog 和任务/路由运行时组合
|
- `app`:运行时组合根,负责依赖注入后的任务/路由组合
|
||||||
- `modules`:按 system/integration/task/payment 维护 Definition 等模块贡献
|
- `modules`:静态模块 catalog,按 system/integration/task/payment 维护 Definition
|
||||||
- `biz/system`:用户、权限、菜单、审计、媒体和系统配置领域
|
- `biz/system`:用户、权限、菜单、审计、媒体和系统配置领域
|
||||||
- `biz/payment`:支付订单、支付流程、支付接口和支付日志
|
- `biz/payment`:支付订单、支付流程、支付接口和支付日志
|
||||||
- `biz/integration`:集成配置定义、校验和连接测试边界
|
- `biz/integration`:集成配置定义、校验和连接测试边界
|
||||||
|
|
@ -27,14 +27,17 @@
|
||||||
定义位于 `modules/system`,JWT 实现集中在 `security`,protobuf JSON 统一使用
|
定义位于 `modules/system`,JWT 实现集中在 `security`,protobuf JSON 统一使用
|
||||||
`pkg/protoutil`。
|
`pkg/protoutil`。
|
||||||
|
|
||||||
`internal/app` 只保留 `catalog.go` 作为组合根:它负责组装模块 catalog、任务
|
`internal/modules/catalog.go` 是静态模块 catalog 的唯一注册点,负责按依赖顺序
|
||||||
注册和运行时。system 的迁移、管理面和定时任务由 `internal/modules/system`
|
汇总各模块 Definition。`internal/app/runtime.go` 只负责依赖注入后的任务注册
|
||||||
自己的 `Definition()` 声明,便于后续业务模块独立接入。
|
和路由运行时组合。这样新增模块只需在 modules catalog 注册一次,app 不再重复
|
||||||
|
维护模块声明。
|
||||||
|
|
||||||
系统表统一使用 `sys_` 前缀;业务表应由新业务模块自行命名和迁移,不要混入
|
系统表统一使用 `sys_` 前缀;业务表应由新业务模块自行命名和迁移,不要混入
|
||||||
本目录。
|
本目录。
|
||||||
|
|
||||||
system 通过 `modules/system.Definition()` 提供系统迁移、通信集成菜单/API 和默认任务;
|
system 通过 `modules/system.Definition()` 提供系统迁移;integration 通过
|
||||||
|
`modules/integration.Definition()` 提供通信集成菜单/API;task 通过
|
||||||
|
`modules/task.Definition()` 提供定时任务迁移和默认任务;
|
||||||
payment 通过 `modules/payment.Definition()` 提供支付迁移及支付菜单/API。两者通过
|
payment 通过 `modules/payment.Definition()` 提供支付迁移及支付菜单/API。两者通过
|
||||||
`worker.TaskMethods` 提供依赖系统用例的任务实现,通过 `server/router.Routes` 提供
|
`worker.TaskMethods` 提供依赖系统用例的任务实现,通过 `server/router.Routes` 提供
|
||||||
路由。静态模块贡献可由 catalog 汇总;带运行时依赖的路由和任务仍需在 cmd/Wire
|
路由。静态模块贡献可由 catalog 汇总;带运行时依赖的路由和任务仍需在 cmd/Wire
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,13 @@
|
||||||
// Package app is the composition root for the running administration service.
|
// Package app is the composition root for the running administration service.
|
||||||
// It knows which business modules are enabled and wires their contributions
|
// It wires runtime objects that need constructed dependencies. Static module
|
||||||
// into the shared platform. Individual modules do not import this package.
|
// declarations live in the sibling internal/modules package.
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
integrationmodule "kra/internal/modules/integration"
|
|
||||||
paymentmodule "kra/internal/modules/payment"
|
|
||||||
systemmodule "kra/internal/modules/system"
|
|
||||||
taskmodule "kra/internal/modules/task"
|
|
||||||
"kra/pkg/module"
|
"kra/pkg/module"
|
||||||
platformtask "kra/pkg/task"
|
platformtask "kra/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Catalog lists the business modules enabled in this binary. Static module
|
|
||||||
// contributions (migrations, admin metadata, default tasks) enter through a
|
|
||||||
// Definition; runtime routes and dependency-bearing task contributors still
|
|
||||||
// need explicit wiring below.
|
|
||||||
func Catalog() module.Catalog {
|
|
||||||
return module.Catalog{Definitions: []module.Definition{
|
|
||||||
systemmodule.Definition(),
|
|
||||||
integrationmodule.Definition(),
|
|
||||||
taskmodule.Definition(),
|
|
||||||
paymentmodule.Definition(),
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskRegistry builds the process-wide registry from dependency-free module
|
// TaskRegistry builds the process-wide registry from dependency-free module
|
||||||
// contributions. Dependency-bearing methods are added by their module runtime
|
// contributions. Dependency-bearing methods are added by their module runtime
|
||||||
// constructors after the usecases have been created.
|
// constructors after the usecases have been created.
|
||||||
|
|
@ -10,37 +10,6 @@ import (
|
||||||
platformtask "kra/pkg/task"
|
platformtask "kra/pkg/task"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCatalogMigrationIDsAreUnique(t *testing.T) {
|
|
||||||
seen := map[string]string{}
|
|
||||||
for _, definition := range Catalog().Definitions {
|
|
||||||
for _, step := range definition.Migrations {
|
|
||||||
if previous, exists := seen[step.ID]; exists {
|
|
||||||
t.Fatalf("migration %q is declared by both %s and %s", step.ID, previous, definition.Name)
|
|
||||||
}
|
|
||||||
seen[step.ID] = definition.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCatalogIncludesSystemDefinition(t *testing.T) {
|
|
||||||
catalog := Catalog()
|
|
||||||
if len(catalog.Definitions) != 4 {
|
|
||||||
t.Fatalf("definitions = %d, want 4", len(catalog.Definitions))
|
|
||||||
}
|
|
||||||
if catalog.Definitions[0].Name != "system" || catalog.Definitions[1].Name != "integration" || catalog.Definitions[2].Name != "task" || catalog.Definitions[3].Name != "payment" {
|
|
||||||
t.Fatalf("definition order = [%q, %q, %q, %q], want [system, integration, task, payment]", catalog.Definitions[0].Name, catalog.Definitions[1].Name, catalog.Definitions[2].Name, catalog.Definitions[3].Name)
|
|
||||||
}
|
|
||||||
if got := catalog.MigrationSteps(); len(got) != 8 {
|
|
||||||
t.Fatalf("module migrations = %d, want 8", len(got))
|
|
||||||
}
|
|
||||||
if surface := catalog.Surface(); len(surface.Menus) != 3 || len(surface.APIs) != 15 {
|
|
||||||
t.Fatalf("admin surface = %d menus/%d APIs, want 3/15", len(surface.Menus), len(surface.APIs))
|
|
||||||
}
|
|
||||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
|
||||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTaskRegistryRegistersStaticModuleMethods(t *testing.T) {
|
func TestTaskRegistryRegistersStaticModuleMethods(t *testing.T) {
|
||||||
method := platformtask.Method{
|
method := platformtask.Method{
|
||||||
Name: "test.static",
|
Name: "test.static",
|
||||||
|
|
@ -25,7 +25,8 @@ reload locks. Do not split them into packages only to reduce file count.
|
||||||
- `system` 只拥有 `sys_*` 系统表、系统仓储、种子和系统维护清理;
|
- `system` 只拥有 `sys_*` 系统表、系统仓储、种子和系统维护清理;
|
||||||
- `integration` 唯一拥有 `sys_integration_configs` 表模型、配置仓储和通信默认值;
|
- `integration` 唯一拥有 `sys_integration_configs` 表模型、配置仓储和通信默认值;
|
||||||
- `task` 只拥有定时任务与任务日志表;
|
- `task` 只拥有定时任务与任务日志表;
|
||||||
- `payment` 只拥有 `pay_orders` 等支付持久化,读取集成配置时复用 integration 的表模型。
|
- `payment` 只拥有 `pay_orders` 等支付持久化,通过 `biz/integration.PaymentConfigReader`
|
||||||
|
读取支付配置,不感知 integration 的 PO 或表结构。
|
||||||
|
|
||||||
配置文件迁移、数据库切换、Redis/Mongo/对象存储重载仍属于根 data 的生命周期编排,
|
配置文件迁移、数据库切换、Redis/Mongo/对象存储重载仍属于根 data 的生命周期编排,
|
||||||
不等同于某个业务模块的表仓储。
|
不等同于某个业务模块的表仓储。
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
|
||||||
integrationbiz "kra/internal/biz/integration"
|
integrationbiz "kra/internal/biz/integration"
|
||||||
"kra/pkg/database/migration"
|
"kra/pkg/database/migration"
|
||||||
|
|
@ -15,6 +16,7 @@ func Migrations() []migration.Step {
|
||||||
return migration.CreateMissingTables(db, &ConfigPO{})
|
return migration.CreateMissingTables(db, &ConfigPO{})
|
||||||
}},
|
}},
|
||||||
{ID: "202608210001_communication_integration_defaults", Migrate: ensureCommunicationIntegrationConfigs},
|
{ID: "202608210001_communication_integration_defaults", Migrate: ensureCommunicationIntegrationConfigs},
|
||||||
|
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,3 +45,43 @@ func ensureCommunicationIntegrationConfigs(db *gorm.DB) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensurePaymentIntegrationConfigs(db *gorm.DB) error {
|
||||||
|
for _, definition := range integrationbiz.IntegrationDefinitions(integrationbiz.IntegrationKindPayment) {
|
||||||
|
provider := definition.Provider
|
||||||
|
var row ConfigPO
|
||||||
|
err := db.Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindPayment, provider).First(&row).Error
|
||||||
|
defaults := integrationbiz.DefaultIntegrationConfig(integrationbiz.IntegrationKindPayment, provider)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
encoded, marshalErr := json.Marshal(defaults)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
if err := db.Create(&ConfigPO{Kind: integrationbiz.IntegrationKindPayment, Provider: provider, Config: string(encoded)}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
values := integrationObject(json.RawMessage(row.Config))
|
||||||
|
changed := false
|
||||||
|
for key, value := range defaults {
|
||||||
|
if _, exists := values[key]; !exists {
|
||||||
|
values[key] = value
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
encoded, marshalErr := json.Marshal(values)
|
||||||
|
if marshalErr != nil {
|
||||||
|
return marshalErr
|
||||||
|
}
|
||||||
|
if err := db.Model(&row).Update("config", string(encoded)).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,15 @@ package data
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
bizpayment "kra/internal/biz/payment"
|
||||||
dataintegration "kra/internal/data/integration"
|
dataintegration "kra/internal/data/integration"
|
||||||
integrationmodule "kra/internal/modules/integration"
|
"kra/internal/modules"
|
||||||
paymentmodule "kra/internal/modules/payment"
|
|
||||||
systemmodule "kra/internal/modules/system"
|
|
||||||
taskmodule "kra/internal/modules/task"
|
|
||||||
"kra/pkg/database/migration"
|
"kra/pkg/database/migration"
|
||||||
platformmodule "kra/pkg/module"
|
platformmodule "kra/pkg/module"
|
||||||
)
|
)
|
||||||
|
|
||||||
func testCatalog() platformmodule.Catalog {
|
func testCatalog() platformmodule.Catalog {
|
||||||
return platformmodule.Catalog{Definitions: []platformmodule.Definition{
|
return modules.Catalog()
|
||||||
systemmodule.Definition(), integrationmodule.Definition(), taskmodule.Definition(), paymentmodule.Definition(),
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||||
|
|
@ -48,6 +44,13 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||||
if len(communicationRows) != 3 {
|
if len(communicationRows) != 3 {
|
||||||
t.Fatalf("communication integration rows = %d, want 3", len(communicationRows))
|
t.Fatalf("communication integration rows = %d, want 3", len(communicationRows))
|
||||||
}
|
}
|
||||||
|
var paymentRows int64
|
||||||
|
if err = db.Model(&dataintegration.ConfigPO{}).Where("kind = ?", "payment").Count(&paymentRows).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if paymentRows != int64(len(bizpayment.SupportedPaymentProviders)) {
|
||||||
|
t.Fatalf("payment integration rows = %d, want %d", paymentRows, len(bizpayment.SupportedPaymentProviders))
|
||||||
|
}
|
||||||
for _, row := range communicationRows {
|
for _, row := range communicationRows {
|
||||||
if row.Enabled || row.Config == "" {
|
if row.Enabled || row.Config == "" {
|
||||||
t.Fatalf("default communication integration = %#v", row)
|
t.Fatalf("default communication integration = %#v", row)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,5 @@ func Migrations() []migration.Step {
|
||||||
{ID: "202608200003_payment_schema", Migrate: func(db *gorm.DB) error {
|
{ID: "202608200003_payment_schema", Migrate: func(db *gorm.DB) error {
|
||||||
return migration.CreateMissingTables(db, &paymentOrderPO{})
|
return migration.CreateMissingTables(db, &paymentOrderPO{})
|
||||||
}},
|
}},
|
||||||
{ID: "202608200004_payment_defaults", Migrate: ensurePaymentIntegrationConfigs},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -271,16 +271,22 @@ func recordPaymentTestError(ctx context.Context, data Provider, provider, tradeN
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]any, error) {
|
func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]any, error) {
|
||||||
var row dataintegration.ConfigPO
|
if r == nil || r.config == nil {
|
||||||
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationbiz.IntegrationKindPayment, provider).First(&row).Error; err != nil {
|
return nil, errors.New("支付配置仓储未接入")
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
}
|
||||||
|
config, err := r.config.ReadPaymentConfig(ctx, provider)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, integrationbiz.ErrPaymentConfigNotFound) {
|
||||||
return nil, bizpayment.ErrPaymentProviderNotFound
|
return nil, bizpayment.ErrPaymentProviderNotFound
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if config == nil {
|
||||||
|
return nil, errors.New("支付配置为空")
|
||||||
|
}
|
||||||
values := map[string]any{}
|
values := map[string]any{}
|
||||||
if err := json.Unmarshal([]byte(row.Config), &values); err != nil {
|
if err := json.Unmarshal(config.Values, &values); err != nil || values == nil {
|
||||||
return nil, fmt.Errorf("支付配置格式错误: %w", err)
|
return nil, errors.New("支付配置格式错误")
|
||||||
}
|
}
|
||||||
return values, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,9 @@ package payment
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/glebarez/sqlite"
|
"github.com/glebarez/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
dataintegration "kra/internal/data/integration"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Data struct{ gormDB *reloadableDB }
|
type Data struct{ gormDB *reloadableDB }
|
||||||
|
|
@ -30,32 +28,3 @@ func openWithDriver(driver, dsn string) (*gorm.DB, error) {
|
||||||
}
|
}
|
||||||
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
return gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func openIntegrationConfigTestDB(t *testing.T) *gorm.DB {
|
|
||||||
t.Helper()
|
|
||||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := db.AutoMigrate(&dataintegration.ConfigPO{}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
if sqlDB, err := db.DB(); err == nil {
|
|
||||||
_ = sqlDB.Close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateAll(db *gorm.DB) error {
|
|
||||||
if err := db.AutoMigrate(&dataintegration.ConfigPO{}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, step := range Migrations() {
|
|
||||||
if err := step.Migrate(db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ func (i *tokenIssuer) ReissueToken(source *system.AuthClaims, authorityID uint)
|
||||||
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
|
AuthorityID: authorityID, BufferTime: int64(source.BufferTime / time.Second), UserType: source.UserType,
|
||||||
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
|
MustChangePwd: source.MustChangePwd, PasswordVersion: source.PasswordVersion,
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
Audience: jwt.ClaimStrings(append([]string(nil), source.Audience...)), Issuer: source.Issuer,
|
Audience: jwt.ClaimStrings{security.TokenAudience}, Issuer: settings.Issuer,
|
||||||
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
IssuedAt: jwt.NewNumericDate(source.IssuedAt), NotBefore: jwt.NewNumericDate(source.NotBefore), ExpiresAt: jwt.NewNumericDate(source.ExpiresAt),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +124,8 @@ func (i *tokenIssuer) ReissueToken(source *system.AuthClaims, authorityID uint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
|
func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
|
||||||
claims, err := security.Parse(token, i.settings.JWTSettings().SigningKey)
|
settings := i.settings.JWTSettings()
|
||||||
|
claims, err := security.ParseWithIssuer(token, settings.SigningKey, settings.Issuer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, security.ErrTokenExpired):
|
case errors.Is(err, security.ErrTokenExpired):
|
||||||
|
|
@ -144,5 +145,12 @@ func (i *tokenIssuer) ParseToken(token string) (*system.AuthClaims, error) {
|
||||||
if claims.IssuedAt != nil {
|
if claims.IssuedAt != nil {
|
||||||
issuedAt = claims.IssuedAt.Time
|
issuedAt = claims.IssuedAt.Time
|
||||||
}
|
}
|
||||||
return &system.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: claims.NotBefore.Time, ExpiresAt: claims.ExpiresAt.Time}, nil
|
notBefore, expiresAt := time.Time{}, time.Time{}
|
||||||
|
if claims.NotBefore != nil {
|
||||||
|
notBefore = claims.NotBefore.Time
|
||||||
|
}
|
||||||
|
if claims.ExpiresAt != nil {
|
||||||
|
expiresAt = claims.ExpiresAt.Time
|
||||||
|
}
|
||||||
|
return &system.AuthClaims{UUID: claims.UUID, ID: claims.ID, Username: claims.Username, NickName: claims.NickName, AuthorityID: claims.AuthorityID, UserType: claims.UserType, BufferTime: time.Duration(claims.BufferTime) * time.Second, MustChangePwd: claims.MustChangePwd, PasswordVersion: claims.PasswordVersion, Issuer: claims.Issuer, Audience: audience, IssuedAt: issuedAt, NotBefore: notBefore, ExpiresAt: expiresAt}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ func (s *s3Storage) DeletePrefix(ctx context.Context, prefix string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: boundedPrefix(s.key(prefix), prefix), Recursive: true})
|
items := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{Prefix: boundedPrefix(s.key(prefix+"/"), prefix+"/"), Recursive: true})
|
||||||
for item := range items {
|
for item := range items {
|
||||||
if item.Err != nil {
|
if item.Err != nil {
|
||||||
return item.Err
|
return item.Err
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Package modules owns the static declarations of built-in business modules.
|
||||||
|
// It is intentionally separate from app: modules describe what they provide,
|
||||||
|
// while app composes runtime objects that need constructed dependencies.
|
||||||
|
package modules
|
||||||
|
|
||||||
|
import (
|
||||||
|
integrationmodule "kra/internal/modules/integration"
|
||||||
|
paymentmodule "kra/internal/modules/payment"
|
||||||
|
systemmodule "kra/internal/modules/system"
|
||||||
|
taskmodule "kra/internal/modules/task"
|
||||||
|
"kra/pkg/module"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Catalog returns the modules enabled in this binary in dependency order.
|
||||||
|
// Keep this as the single static registration point for migrations, admin
|
||||||
|
// metadata and dependency-free timed tasks.
|
||||||
|
func Catalog() module.Catalog {
|
||||||
|
return module.Catalog{Definitions: []module.Definition{
|
||||||
|
systemmodule.Definition(),
|
||||||
|
integrationmodule.Definition(),
|
||||||
|
taskmodule.Definition(),
|
||||||
|
paymentmodule.Definition(),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package modules
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestCatalogMigrationIDsAreUnique(t *testing.T) {
|
||||||
|
seen := map[string]string{}
|
||||||
|
for _, definition := range Catalog().Definitions {
|
||||||
|
for _, step := range definition.Migrations {
|
||||||
|
if previous, exists := seen[step.ID]; exists {
|
||||||
|
t.Fatalf("migration %q is declared by both %s and %s", step.ID, previous, definition.Name)
|
||||||
|
}
|
||||||
|
seen[step.ID] = definition.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||||
|
catalog := Catalog()
|
||||||
|
if len(catalog.Definitions) != 4 {
|
||||||
|
t.Fatalf("definitions = %d, want 4", len(catalog.Definitions))
|
||||||
|
}
|
||||||
|
want := []string{"system", "integration", "task", "payment"}
|
||||||
|
for index, name := range want {
|
||||||
|
if got := catalog.Definitions[index].Name; got != name {
|
||||||
|
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := catalog.MigrationSteps(); len(got) != 8 {
|
||||||
|
t.Fatalf("module migrations = %d, want 8", len(got))
|
||||||
|
}
|
||||||
|
if surface := catalog.Surface(); len(surface.Menus) != 3 || len(surface.APIs) != 15 {
|
||||||
|
t.Fatalf("admin surface = %d menus/%d APIs, want 3/15", len(surface.Menus), len(surface.APIs))
|
||||||
|
}
|
||||||
|
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||||
|
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import "testing"
|
||||||
|
|
||||||
func TestDefinitionOwnsCommunicationSurface(t *testing.T) {
|
func TestDefinitionOwnsCommunicationSurface(t *testing.T) {
|
||||||
definition := Definition()
|
definition := Definition()
|
||||||
if definition.Name != "integration" || len(definition.Surface.Menus) != 1 || len(definition.Surface.APIs) != 5 {
|
if definition.Name != "integration" || len(definition.Migrations) != 3 || len(definition.Surface.Menus) != 1 || len(definition.Surface.APIs) != 5 {
|
||||||
t.Fatalf("integration surface = %#v", definition.Surface)
|
t.Fatalf("integration surface = %#v", definition.Surface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ func TestDefinitionOwnsPaymentContributions(t *testing.T) {
|
||||||
if definition.Name != "payment" {
|
if definition.Name != "payment" {
|
||||||
t.Fatalf("name = %q, want payment", definition.Name)
|
t.Fatalf("name = %q, want payment", definition.Name)
|
||||||
}
|
}
|
||||||
if len(definition.Migrations) != 2 {
|
if len(definition.Migrations) != 1 {
|
||||||
t.Fatalf("migrations = %d, want 2", len(definition.Migrations))
|
t.Fatalf("migrations = %d, want 1", len(definition.Migrations))
|
||||||
}
|
}
|
||||||
if len(definition.Surface.Menus) != 2 || len(definition.Surface.APIs) != 10 {
|
if len(definition.Surface.Menus) != 2 || len(definition.Surface.APIs) != 10 {
|
||||||
t.Fatalf("surface = %d menus/%d APIs, want 2/10", len(definition.Surface.Menus), len(definition.Surface.APIs))
|
t.Fatalf("surface = %d menus/%d APIs, want 2/10", len(definition.Surface.Menus), len(definition.Surface.APIs))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Internal Security
|
||||||
|
|
||||||
|
This package owns the application's administrator JWT policy. It is kept
|
||||||
|
inside `internal` because its claims, audience, issuer and password-version
|
||||||
|
fields are KRA-specific security protocol, not a reusable JWT utility.
|
||||||
|
|
||||||
|
Generic stateless helpers may live in `pkg`, but admin token signing and
|
||||||
|
verification stay here so other services cannot accidentally depend on this
|
||||||
|
application's security contract.
|
||||||
|
|
@ -7,7 +7,11 @@ import (
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const TokenAudience = "KRA"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
ErrEmptySigningKey = errors.New("empty JWT signing key")
|
||||||
|
ErrInvalidClaims = errors.New("invalid token claims")
|
||||||
ErrTokenExpired = errors.New("token expired")
|
ErrTokenExpired = errors.New("token expired")
|
||||||
ErrTokenMalformed = errors.New("token malformed")
|
ErrTokenMalformed = errors.New("token malformed")
|
||||||
ErrTokenSignatureInvalid = errors.New("token signature invalid")
|
ErrTokenSignatureInvalid = errors.New("token signature invalid")
|
||||||
|
|
@ -29,29 +33,54 @@ type Claims struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Generate(secret, issuer string, expires, buffer time.Duration, userID, authorityID uint, uuid, username, nickname string, mustChange bool, passwordVersion int64) (string, *Claims, error) {
|
func Generate(secret, issuer string, expires, buffer time.Duration, userID, authorityID uint, uuid, username, nickname string, mustChange bool, passwordVersion int64) (string, *Claims, error) {
|
||||||
if secret == "" {
|
if err := validateSigningOptions(secret, expires, buffer); err != nil {
|
||||||
return "", nil, errors.New("empty JWT signing key")
|
return "", nil, err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), UserType: "admin", MustChangePwd: mustChange, PasswordVersion: passwordVersion, RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"KRA"}, Issuer: issuer, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-1000)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}}
|
claims := &Claims{UUID: uuid, ID: userID, Username: username, NickName: nickname, AuthorityID: authorityID, BufferTime: int64(buffer / time.Second), UserType: "admin", MustChangePwd: mustChange, PasswordVersion: passwordVersion, RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{TokenAudience}, Issuer: issuer, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(now.Add(expires))}}
|
||||||
token, err := Sign(secret, claims)
|
token, err := Sign(secret, claims)
|
||||||
return token, claims, err
|
return token, claims, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Sign(secret string, claims *Claims) (string, error) {
|
func Sign(secret string, claims *Claims) (string, error) {
|
||||||
if secret == "" {
|
if secret == "" {
|
||||||
return "", errors.New("empty JWT signing key")
|
return "", ErrEmptySigningKey
|
||||||
|
}
|
||||||
|
if claims == nil {
|
||||||
|
return "", ErrInvalidClaims
|
||||||
}
|
}
|
||||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
|
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret))
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse(tokenString, secret string) (*Claims, error) {
|
func Parse(tokenString, secret string) (*Claims, error) {
|
||||||
|
return parse(tokenString, secret, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseWithIssuer validates a KRA admin token and its configured issuer.
|
||||||
|
func ParseWithIssuer(tokenString, secret, issuer string) (*Claims, error) {
|
||||||
|
return parse(tokenString, secret, issuer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(tokenString, secret, issuer string) (*Claims, error) {
|
||||||
|
if secret == "" {
|
||||||
|
return nil, ErrEmptySigningKey
|
||||||
|
}
|
||||||
|
options := []jwt.ParserOption{
|
||||||
|
jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}),
|
||||||
|
jwt.WithAudience(TokenAudience),
|
||||||
|
jwt.WithExpirationRequired(),
|
||||||
|
jwt.WithNotBeforeRequired(),
|
||||||
|
jwt.WithLeeway(time.Second),
|
||||||
|
}
|
||||||
|
if issuer != "" {
|
||||||
|
options = append(options, jwt.WithIssuer(issuer))
|
||||||
|
}
|
||||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (any, error) {
|
||||||
if token.Method != jwt.SigningMethodHS256 {
|
if token.Method != jwt.SigningMethodHS256 {
|
||||||
return nil, errors.New("unexpected signing method")
|
return nil, errors.New("unexpected signing method")
|
||||||
}
|
}
|
||||||
return []byte(secret), nil
|
return []byte(secret), nil
|
||||||
})
|
}, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, jwt.ErrTokenExpired):
|
case errors.Is(err, jwt.ErrTokenExpired):
|
||||||
|
|
@ -71,7 +100,20 @@ func Parse(tokenString, secret string) (*Claims, error) {
|
||||||
}
|
}
|
||||||
claims, ok := token.Claims.(*Claims)
|
claims, ok := token.Claims.(*Claims)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("token claims are invalid")
|
return nil, ErrInvalidClaims
|
||||||
}
|
}
|
||||||
return claims, nil
|
return claims, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateSigningOptions(secret string, expires, buffer time.Duration) error {
|
||||||
|
if secret == "" {
|
||||||
|
return ErrEmptySigningKey
|
||||||
|
}
|
||||||
|
if expires <= 0 {
|
||||||
|
return errors.New("JWT expiration must be positive")
|
||||||
|
}
|
||||||
|
if buffer < 0 {
|
||||||
|
return errors.New("JWT buffer must not be negative")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateAndParseWithIssuerRoundTrip(t *testing.T) {
|
||||||
|
token, want, err := Generate("secret", "kra-admin", time.Hour, time.Minute, 7, 8, "uuid", "alice", "Alice", true, 9)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got, err := ParseWithIssuer(token, "secret", "kra-admin")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got.ID != want.ID || got.Username != want.Username || got.Audience[0] != TokenAudience {
|
||||||
|
t.Fatalf("claims = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRejectsWrongIssuerAndAudience(t *testing.T) {
|
||||||
|
token, _, err := Generate("secret", "kra-admin", time.Hour, time.Minute, 1, 1, "uuid", "user", "", false, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = ParseWithIssuer(token, "secret", "other"); !errors.Is(err, ErrTokenInvalid) {
|
||||||
|
t.Fatalf("wrong issuer error = %v, want ErrTokenInvalid", err)
|
||||||
|
}
|
||||||
|
claims := &Claims{RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"other"}, Issuer: "kra-admin", IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour))}}
|
||||||
|
token, err = Sign("secret", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = ParseWithIssuer(token, "secret", "kra-admin"); !errors.Is(err, ErrTokenInvalid) {
|
||||||
|
t.Fatalf("wrong audience error = %v, want ErrTokenInvalid", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRejectsMissingRequiredTimeClaims(t *testing.T) {
|
||||||
|
claims := &Claims{RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{TokenAudience}, Issuer: "kra-admin"}}
|
||||||
|
token, err := Sign("secret", claims)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = ParseWithIssuer(token, "secret", "kra-admin"); !errors.Is(err, ErrTokenInvalid) {
|
||||||
|
t.Fatalf("missing time claims error = %v, want ErrTokenInvalid", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSigningOptionsRejectInvalidValues(t *testing.T) {
|
||||||
|
if _, _, err := Generate("", "kra", time.Hour, 0, 1, 1, "", "", "", false, 0); !errors.Is(err, ErrEmptySigningKey) {
|
||||||
|
t.Fatalf("empty key error = %v", err)
|
||||||
|
}
|
||||||
|
if _, _, err := Generate("secret", "kra", 0, 0, 1, 1, "", "", "", false, 0); err == nil {
|
||||||
|
t.Fatal("zero expiration was accepted")
|
||||||
|
}
|
||||||
|
if _, err := Sign("secret", nil); !errors.Is(err, ErrInvalidClaims) {
|
||||||
|
t.Fatalf("nil claims error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRejectsAlgorithmConfusion(t *testing.T) {
|
||||||
|
claims := &Claims{RegisteredClaims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{TokenAudience}, Issuer: "kra-admin", IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Second)), ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour))}}
|
||||||
|
token, err := jwt.NewWithClaims(jwt.SigningMethodHS512, claims).SignedString([]byte("secret"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err = ParseWithIssuer(token, "secret", "kra-admin"); !errors.Is(err, ErrTokenSignatureInvalid) {
|
||||||
|
t.Fatalf("algorithm confusion error = %v, want ErrTokenSignatureInvalid", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue