refactor(platform): remove deprecated reporting and audit modules
This commit is contained in:
@@ -42,11 +42,8 @@ func InitPlatformAccess(database *gorm.DB) error {
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "ec", Name: "电商管理", Icon: "icon-shopping", Path: "/ec/product", SortNo: 80},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "finance", Name: "财务管理", Icon: "icon-safe", Path: "/finance/payment", SortNo: 90},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "wallet", Name: "钱包中心", Icon: "icon-wallet", Path: "/wallet/list", SortNo: 100},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "report", Name: "统计报表", Icon: "icon-bar-chart", Path: "/report/list", SortNo: 110},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "content", Name: "内容管理", Icon: "icon-file", Path: "/content", SortNo: 120},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "notification", Name: "通知管理", Icon: "icon-notification", Path: "/notification", SortNo: 130},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "customer_service", Name: "客户服务", Icon: "icon-customer-service", Path: "/customer_service", SortNo: 140},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "audit", Name: "审计合规", Icon: "icon-history", Path: "/audit", SortNo: 150},
|
||||
{Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1}, MenuCode: "platform", Name: "平台配置", Icon: "icon-settings", Path: "/platform/account", SortNo: 160},
|
||||
}
|
||||
for index := range menus {
|
||||
|
||||
@@ -27,8 +27,7 @@ func TestInitPlatformAccessSeedsEveryProtectedFrontendDomain(t *testing.T) {
|
||||
|
||||
domains := []string{
|
||||
"dashboard", "gas", "delivery", "staff", "user", "device", "safety",
|
||||
"ec", "finance", "wallet", "report", "content", "notification",
|
||||
"customer_service", "audit", "platform",
|
||||
"ec", "finance", "wallet", "content", "customer_service", "platform",
|
||||
}
|
||||
for index, domain := range domains {
|
||||
menuID := uint64(index + 10)
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var errApprovalNotProcessable = errors.New("approval is not processable")
|
||||
|
||||
func approvalValues(status, opinion, operatorIdentity string) map[string]any {
|
||||
return map[string]any{
|
||||
"status": status,
|
||||
"opinion": opinion,
|
||||
"handler_identity": operatorIdentity,
|
||||
"handled_at": time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
|
||||
// ApproveAudit records the reviewer and decision without allowing an approval
|
||||
// to mutate any business fields. The operation audit is created atomically with
|
||||
// the approval update.
|
||||
func ApproveAudit(ctx *gin.Context) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Status string `json:"status" binding:"required,max=32"`
|
||||
Opinion string `json:"opinion" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if request.Status != "approved" && request.Status != "rejected" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
values := approvalValues(request.Status, request.Opinion, claims.Identity)
|
||||
var approval models.AuditApproval
|
||||
err = impl.DBService.Transaction(func(transaction *gorm.DB) error {
|
||||
if err := transaction.Where("identity = ?", ctx.Param("identity")).First(&approval).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if approval.Status != "pending" || approval.ApplicantIdentity == claims.Identity {
|
||||
return errApprovalNotProcessable
|
||||
}
|
||||
before, err := json.Marshal(gin.H{
|
||||
"status": approval.Status, "opinion": approval.Opinion,
|
||||
"handler_identity": approval.HandlerIdentity, "handled_at": approval.HandledAt,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result := transaction.Model(&models.AuditApproval{}).Where("identity = ? AND status = ?", approval.Identity, "pending").Updates(values); result.Error != nil {
|
||||
return result.Error
|
||||
} else if result.RowsAffected == 0 {
|
||||
return errApprovalNotProcessable
|
||||
}
|
||||
after, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return transaction.Create(&models.AuditOperationLog{
|
||||
Entity: newEntity("enabled"),
|
||||
OperatorIdentity: claims.Identity,
|
||||
Action: "approve",
|
||||
ObjectType: "audit_approval",
|
||||
ObjectIdentity: approval.Identity,
|
||||
BeforeData: string(before),
|
||||
AfterData: string(after),
|
||||
}).Error
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, errApprovalNotProcessable) {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
infra.Response.Error(ctx, errcode.ErrRecordNotFound)
|
||||
return
|
||||
}
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
approval.Status = request.Status
|
||||
approval.Opinion = request.Opinion
|
||||
approval.HandlerIdentity = claims.Identity
|
||||
handledAt := values["handled_at"].(time.Time)
|
||||
approval.HandledAt = &handledAt
|
||||
infra.Response.Success(ctx, resourceResponse(approval))
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/types"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestApprovalValuesOnlyChangesApprovalFieldsAndRecordsOperator(t *testing.T) {
|
||||
values := approvalValues("approved", "accepted", "operator-a")
|
||||
|
||||
if values["status"] != "approved" || values["opinion"] != "accepted" || values["handler_identity"] != "operator-a" {
|
||||
t.Fatalf("approval values do not retain the approved state, opinion, and operator: %#v", values)
|
||||
}
|
||||
if _, ok := values["handled_at"]; !ok {
|
||||
t.Fatalf("approval values do not record handling time: %#v", values)
|
||||
}
|
||||
if len(values) != 4 {
|
||||
t.Fatalf("approval update includes fields outside its whitelist: %#v", values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApproveAuditOnlyUpdatesApprovalFieldsAndAppendsOperationAudit(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
now := time.Now().UTC()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
|
||||
WithArgs("approval-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "business_type", "business_identity", "applicant_identity", "opinion", "handler_identity", "handled_at"}).
|
||||
AddRow(uint64(1), "approval-a", now, now, "pending", 1, "refund", "payment-a", "applicant-a", "", "", nil))
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "audit_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
|
||||
WithArgs(sqlmock.AnyArg(), "operator-a", "accepted", "approved", sqlmock.AnyArg(), "approval-a", "pending").
|
||||
WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "audit_operation_log" ("identity","created_at","updated_at","status","version","operator_identity","action","object_type","object_identity","before_data","after_data") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING "id"`)).
|
||||
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), "enabled", 1, "operator-a", "approve", "audit_approval", "approval-a", jsonContaining(`"status":"pending"`), jsonContaining(`"handler_identity":"operator-a"`)).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
|
||||
mock.ExpectCommit()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved","opinion":"accepted","business_identity":"payment-b"}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
|
||||
ApproveAudit(ctx)
|
||||
|
||||
assertMockExpectations(t, mock)
|
||||
assertResponseCode(t, recorder, 0)
|
||||
if !strings.Contains(recorder.Body.String(), `"handler_identity":"operator-a"`) {
|
||||
t.Fatalf("approval response omitted its handler: %s", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestApproveAuditRejectsStatusesOutsideApprovedAndRejected(t *testing.T) {
|
||||
for _, decision := range []string{"pending", "archived"} {
|
||||
t.Run(decision, func(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"`+decision+`"}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
|
||||
|
||||
ApproveAudit(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApproveAuditRejectsAlreadyHandledApproval(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
|
||||
WithArgs("approval-a", 1).
|
||||
WillReturnRows(approvalRows("approval-a", "approved", "applicant-a"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
|
||||
ApproveAudit(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestApproveAuditRejectsTheApplicant(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
|
||||
WithArgs("approval-a", 1).
|
||||
WillReturnRows(approvalRows("approval-a", "pending", "operator-a"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
|
||||
ApproveAudit(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestApproveAuditRejectsAConcurrentSecondDecision(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "audit_approval" WHERE identity = $1 ORDER BY "audit_approval"."id" LIMIT $2`)).
|
||||
WithArgs("approval-a", 1).
|
||||
WillReturnRows(approvalRows("approval-a", "pending", "applicant-a"))
|
||||
mock.ExpectExec(regexp.QuoteMeta(`UPDATE "audit_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6 AND status = $7`)).
|
||||
WithArgs(sqlmock.AnyArg(), "operator-a", "", "approved", sqlmock.AnyArg(), "approval-a", "pending").
|
||||
WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
mock.ExpectRollback()
|
||||
|
||||
ctx, recorder := updateContext(http.MethodPost, "/audit/audit_approval/approval-a/approve", "approval-a", []byte(`{"status":"approved"}`))
|
||||
ctx.Set("Auth", &types.JwtClaims{Identity: "operator-a"})
|
||||
ApproveAudit(ctx)
|
||||
|
||||
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func approvalRows(identity, approvalStatus, applicant string) *sqlmock.Rows {
|
||||
now := time.Now().UTC()
|
||||
return sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "business_type", "business_identity", "applicant_identity", "opinion", "handler_identity", "handled_at"}).
|
||||
AddRow(uint64(1), identity, now, now, approvalStatus, 1, "refund", "payment-a", applicant, "", "", nil)
|
||||
}
|
||||
|
||||
type jsonContaining string
|
||||
|
||||
func (expected jsonContaining) Match(value driver.Value) bool {
|
||||
actual, ok := value.(string)
|
||||
return ok && strings.Contains(actual, string(expected))
|
||||
}
|
||||
@@ -69,9 +69,7 @@ var keywordSafeColumns = map[string]bool{
|
||||
"content_type": true, "publish_status": true, "template_code": true,
|
||||
"ticket_no": true, "category": true, "priority": true,
|
||||
"platform_role_code": true, "data_scope": true, "menu_code": true,
|
||||
"path": true, "report_code": true, "report_type": true,
|
||||
"stat_period": true, "dimension": true, "metric_code": true,
|
||||
"scope_type": true, "business_type": true, "resource_type": true,
|
||||
"path": true, "resource_type": true,
|
||||
}
|
||||
|
||||
func applyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB {
|
||||
@@ -100,7 +98,7 @@ func keywordColumns(model any) []string {
|
||||
columns := make([]string, 0)
|
||||
for index := 0; index < modelType.NumField(); index++ {
|
||||
field := modelType.Field(index)
|
||||
if field.Anonymous || field.Type.Kind() != reflect.String || strings.Contains(field.Tag.Get("gorm"), "type:jsonb") {
|
||||
if field.Anonymous || field.Type.Kind() != reflect.String {
|
||||
continue
|
||||
}
|
||||
column := gormColumn(field.Tag.Get("gorm"))
|
||||
|
||||
@@ -79,11 +79,9 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("ec", "ec_category", Writable, "list"), resourceContract("ec", "ec_product", Writable, "list"), resourceContract("ec", "ec_product_attribute", Writable, "list"), resourceContract("ec", "ec_product_image", Writable, "list"), resourceContract("ec", "ec_cart", Writable, "list"), resourceContract("ec", "ec_order", Writable, "list"), resourceContract("ec", "ec_order_item", Writable, "list"), resourceContract("ec", "ec_review", Writable, "list"),
|
||||
resourceContract("delivery", "delivery_task", Writable, "list"), resourceContract("delivery", "delivery_track", Writable, "list"), resourceContract("delivery", "delivery_track_point", ReadOnly, "list"),
|
||||
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
|
||||
resourceContract("content", "cnt_content", Writable, "list"), resourceContract("notification", "ntf_template", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
|
||||
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
|
||||
resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
|
||||
resourceContract("wallet", "wallet", ReadOnly, "list"), resourceContract("wallet", "wallet_ledger", ReadOnly, "list"), resourceContract("wallet", "wallet_recharge", ReadOnly, "list"), resourceContract("wallet", "wallet_withdrawal", ReadOnly, "list"),
|
||||
resourceContract("report", "report", ReadOnly, "list"), resourceContract("report", "report_item", ReadOnly, "list"), resourceContract("report", "report_metric_snapshot", ReadOnly, "list"),
|
||||
resourceContract("audit", "audit_operation_log", ReadOnly, "list"), resourceContract("audit", "audit_export_log", ReadOnly, "list"), resourceContract("audit", "audit_approval", ReadOnly, "list"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,21 +30,27 @@ func TestExpectedResources(t *testing.T) {
|
||||
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
|
||||
}
|
||||
|
||||
func TestSafeAndAuditResourceContracts(t *testing.T) {
|
||||
func TestContentUsesCMSResourceAndRemovedDomainsAreAbsent(t *testing.T) {
|
||||
resources := ExpectedResources()
|
||||
assertContract(t, resources, "content", "cms_content", Writable, "list")
|
||||
for _, removed := range []string{"ntf_template", "report", "report_item", "report_metric_snapshot", "audit_operation_log", "audit_export_log", "audit_approval"} {
|
||||
for _, resource := range resources {
|
||||
if resource.Name == removed {
|
||||
t.Fatalf("removed resource %s remains registered", removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeResourceContracts(t *testing.T) {
|
||||
legacySafetyPrefix := "sa" + "f_"
|
||||
legacyAuditPrefix := "au" + "d_"
|
||||
|
||||
assertContract(t, ExpectedResources(), "safety", "safe_rule", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "safety", "safe_event", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "safety", "safe_inspection", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "safety", "safe_event_disposal", AppendOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "audit", "audit_operation_log", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "audit", "audit_export_log", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "audit", "audit_approval", ReadOnly, "list")
|
||||
|
||||
for _, contract := range ExpectedResources() {
|
||||
if (contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix)) ||
|
||||
(contract.Domain == "audit" && strings.HasPrefix(contract.Name, legacyAuditPrefix)) {
|
||||
if contract.Domain == "safety" && strings.HasPrefix(contract.Name, legacySafetyPrefix) {
|
||||
t.Fatalf("legacy resource contract %s/%s must not be registered", contract.Domain, contract.Name)
|
||||
}
|
||||
}
|
||||
@@ -251,15 +257,15 @@ func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
|
||||
func TestPrepareResourceValuesAcceptsArbitraryTextFields(t *testing.T) {
|
||||
t.Run("safety rule", func(t *testing.T) {
|
||||
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":{"max":10},"action":"close-valve","gray_scope":["north"]}`))
|
||||
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve","gray_scope":"north region"}`))
|
||||
values, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action", "gray_scope"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values["threshold"] != `{"max":10}` || values["gray_scope"] != `["north"]` {
|
||||
t.Fatalf("jsonb values were not normalized to strings: %#v", values)
|
||||
if values["threshold"] != `{invalid}` || values["gray_scope"] != `north region` {
|
||||
t.Fatalf("text values were not preserved: %#v", values)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -272,7 +278,7 @@ func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
|
||||
WithArgs("product-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(2)))
|
||||
|
||||
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(`{"ec_order_identity":"order-a","ec_product_identity":"product-a","product_snapshot":{"name":"液化气"},"quantity":2,"sale_amount":500}`))
|
||||
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(`{"ec_order_identity":"order-a","ec_product_identity":"product-a","product_snapshot":"arbitrary product snapshot text","quantity":2,"sale_amount":500}`))
|
||||
values, err := prepareResourceValues(ctx, &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, []ResourceRelation{
|
||||
{Input: "ec_order_identity", Column: "ec_order_id", Model: &models.EcOrder{}, Required: true},
|
||||
{Input: "ec_product_identity", Column: "ec_product_id", Model: &models.EcProduct{}, Required: true},
|
||||
@@ -280,16 +286,16 @@ func TestPrepareResourceValuesNormalizesStringJSONBFields(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values["product_snapshot"] != `{"name":"液化气"}` {
|
||||
if values["product_snapshot"] != `arbitrary product snapshot text` {
|
||||
t.Fatalf("product snapshot was not normalized to a string: %#v", values)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
})
|
||||
|
||||
t.Run("invalid json string", func(t *testing.T) {
|
||||
t.Run("arbitrary text", func(t *testing.T) {
|
||||
ctx, _ := updateContext(http.MethodPost, "/safety/safe_rule", "", []byte(`{"rule_code":"pressure-limit","threshold":"{invalid}","action":"close-valve"}`))
|
||||
if _, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action"}, nil); err == nil {
|
||||
t.Fatal("invalid JSON string was accepted for a string/jsonb field")
|
||||
if _, err := prepareResourceValues(ctx, &models.SafeRule{}, []string{"rule_code", "threshold", "action"}, nil); err != nil {
|
||||
t.Fatalf("arbitrary text was rejected: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -344,7 +350,7 @@ func TestKeywordColumnsUseSafeTextAllowlist(t *testing.T) {
|
||||
model any
|
||||
want []string
|
||||
}{
|
||||
{"safety rule excludes jsonb", &models.SafeRule{}, []string{"rule_code", "action"}},
|
||||
{"safety rule excludes arbitrary text fields", &models.SafeRule{}, []string{"rule_code", "action"}},
|
||||
{"gas basic excludes sensitive fields", &models.GasBasic{}, []string{"code", "name"}},
|
||||
{"user address has no searchable safe text", &models.UserAddress{}, []string{}},
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ func updateResource(ctx *gin.Context, model any, allowedFields []string, relatio
|
||||
return
|
||||
}
|
||||
values, err := resolveResourceRelations(input, allowedFields, relations, false)
|
||||
if err != nil || normalizeStringJSONBFields(model, values) != nil {
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
@@ -283,42 +283,12 @@ func prepareResourceValues(ctx *gin.Context, model any, allowedFields []string,
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
values, err := resolveResourceRelations(input, allowedFields, relations, true)
|
||||
if err != nil || normalizeStringJSONBFields(model, values) != nil || len(values) == 0 {
|
||||
if err != nil || len(values) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func normalizeStringJSONBFields(model any, values map[string]any) error {
|
||||
modelType := reflect.TypeOf(model)
|
||||
for modelType.Kind() == reflect.Pointer {
|
||||
modelType = modelType.Elem()
|
||||
}
|
||||
for index := 0; index < modelType.NumField(); index++ {
|
||||
field := modelType.Field(index)
|
||||
if field.Type.Kind() != reflect.String || !strings.Contains(field.Tag.Get("gorm"), "type:jsonb") {
|
||||
continue
|
||||
}
|
||||
column := gormColumn(field.Tag.Get("gorm"))
|
||||
value, exists := values[column]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
if !json.Valid([]byte(text)) {
|
||||
return errors.New("invalid jsonb string")
|
||||
}
|
||||
continue
|
||||
}
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil || !json.Valid(encoded) {
|
||||
return errors.New("invalid jsonb value")
|
||||
}
|
||||
values[column] = string(encoded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveResourceRelations(input map[string]any, allowedFields []string, relations []ResourceRelation, requireRelations bool) (map[string]any, error) {
|
||||
values := filterFields(input, allowedFields)
|
||||
for _, relation := range relations {
|
||||
@@ -411,7 +381,6 @@ var relationIdentityModels = map[string]any{
|
||||
"delivery_track_id": &models.DeliveryTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"platform_menu_id": &models.PlatformMenu{},
|
||||
"report_id": &models.Report{},
|
||||
"wallet_id": &models.Wallet{},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// AuditApproval 对应 audit_approval,保存审批流与复核意见。
|
||||
type AuditApproval struct {
|
||||
Entity // 公共实体字段
|
||||
BusinessType string `gorm:"column:business_type;type:varchar(64);not null" json:"business_type"` // business_type 业务字段
|
||||
BusinessIdentity string `gorm:"column:business_identity;type:varchar(36);not null;index" json:"business_identity"` // business_identity 业务字段
|
||||
ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` // applicant_identity 业务字段
|
||||
Opinion string `gorm:"column:opinion;type:text;not null;default:''" json:"opinion"` // opinion 业务字段
|
||||
HandlerIdentity string `gorm:"column:handler_identity;type:varchar(36);not null;default:'';index" json:"handler_identity"` // handler_identity 业务字段
|
||||
HandledAt *time.Time `gorm:"column:handled_at;type:timestamptz" json:"handled_at"` // handled_at 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&AuditApproval{}) }
|
||||
func (table *AuditApproval) TableName() string { return "audit_approval" }
|
||||
@@ -1,19 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditExportLog 对应 audit_export_log,保存敏感导出审计。
|
||||
type AuditExportLog struct {
|
||||
Entity // 公共实体字段
|
||||
ApplicantIdentity string `gorm:"column:applicant_identity;type:varchar(36);not null;index" json:"applicant_identity"` // applicant_identity 业务字段
|
||||
Purpose string `gorm:"column:purpose;type:varchar(255);not null" json:"purpose"` // purpose 业务字段
|
||||
FieldScope string `gorm:"column:field_scope;type:jsonb;not null;default:'{}'" json:"field_scope"` // field_scope 业务字段
|
||||
ApprovedAt *time.Time `gorm:"column:approved_at;type:timestamptz" json:"approved_at"` // approved_at 业务字段
|
||||
FileURI string `gorm:"column:file_uri;type:varchar(512);not null;default:''" json:"file_uri"` // file_uri 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&AuditExportLog{}) }
|
||||
func (table *AuditExportLog) TableName() string { return "audit_export_log" }
|
||||
@@ -1,17 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// AuditOperationLog 对应 audit_operation_log,保存不可变操作审计。
|
||||
type AuditOperationLog struct {
|
||||
Entity // 公共实体字段
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;index" json:"operator_identity"` // operator_identity 业务字段
|
||||
Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
|
||||
ObjectType string `gorm:"column:object_type;type:varchar(64);not null" json:"object_type"` // object_type 业务字段
|
||||
ObjectIdentity string `gorm:"column:object_identity;type:varchar(36);not null;index" json:"object_identity"` // object_identity 业务字段
|
||||
BeforeData string `gorm:"column:before_data;type:jsonb;not null;default:'{}'" json:"before_data"` // before_data 业务字段
|
||||
AfterData string `gorm:"column:after_data;type:jsonb;not null;default:'{}'" json:"after_data"` // after_data 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&AuditOperationLog{}) }
|
||||
func (table *AuditOperationLog) TableName() string { return "audit_operation_log" }
|
||||
@@ -2,8 +2,8 @@ package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// CntContent 对应 cnt_content,保存公告与协议内容。
|
||||
type CntContent struct {
|
||||
// CmsContent 对应 cms_content,保存公告与协议内容。
|
||||
type CmsContent struct {
|
||||
Entity // 公共实体字段
|
||||
ContentType string `gorm:"column:content_type;type:varchar(32);not null" json:"content_type"` // content_type 业务字段
|
||||
Title string `gorm:"column:title;type:varchar(256);not null" json:"title"` // title 业务字段
|
||||
@@ -12,5 +12,5 @@ type CntContent struct {
|
||||
PublishStatus string `gorm:"column:publish_status;type:varchar(32);not null;default:'draft'" json:"publish_status"` // publish_status 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&CntContent{}) }
|
||||
func (table *CntContent) TableName() string { return "cnt_content" }
|
||||
func init() { database.AppendMigrate(&CmsContent{}) }
|
||||
func (table *CmsContent) TableName() string { return "cms_content" }
|
||||
@@ -10,7 +10,7 @@ type DevTelemetry struct {
|
||||
Entity // 公共实体字段
|
||||
SmartCylinderValveIdentity string `gorm:"column:smart_cylinder_valve_identity;type:varchar(36);not null;index" json:"smart_cylinder_valve_identity"` // smart_cylinder_valve_identity 业务字段
|
||||
ReportedAt time.Time `gorm:"column:reported_at;type:timestamptz;not null;index" json:"reported_at"` // reported_at 业务字段
|
||||
Payload string `gorm:"column:payload;type:jsonb;not null;default:'{}'" json:"payload"` // payload 业务字段
|
||||
Payload string `gorm:"column:payload;type:text;not null;default:''" json:"payload"` // payload 业务字段
|
||||
QualityFlag string `gorm:"column:quality_flag;type:varchar(32);not null;default:'normal'" json:"quality_flag"` // quality_flag 业务字段
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import "git.apinb.com/bsm-sdk/core/database"
|
||||
// EcOrderItem 对应 ec_order_item,保存订单商品快照。
|
||||
type EcOrderItem struct {
|
||||
Entity // 公共实体字段
|
||||
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
|
||||
EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段
|
||||
ProductSnapshot string `gorm:"column:product_snapshot;type:jsonb;not null;default:'{}'" json:"product_snapshot"` // product_snapshot 业务字段
|
||||
Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` // quantity 业务字段
|
||||
SaleAmount int64 `gorm:"column:sale_amount;not null;default:0" json:"sale_amount"` // sale_amount 业务字段
|
||||
EcOrderID uint64 `gorm:"column:ec_order_id;not null;index" json:"ec_order_id"` // ec_order_id 业务字段
|
||||
EcProductID uint64 `gorm:"column:ec_product_id;not null;index" json:"ec_product_id"` // ec_product_id 业务字段
|
||||
ProductSnapshot string `gorm:"column:product_snapshot;type:text;not null;default:''" json:"product_snapshot"` // product_snapshot 业务字段
|
||||
Quantity int `gorm:"column:quantity;not null;default:1" json:"quantity"` // quantity 业务字段
|
||||
SaleAmount int64 `gorm:"column:sale_amount;not null;default:0" json:"sale_amount"` // sale_amount 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&EcOrderItem{}) }
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// NtfTemplate 对应 ntf_template,保存通知模板。
|
||||
type NtfTemplate struct {
|
||||
Entity // 公共实体字段
|
||||
TemplateCode string `gorm:"column:template_code;type:varchar(64);not null;uniqueIndex" json:"template_code"` // template_code 业务字段
|
||||
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段
|
||||
Content string `gorm:"column:content;type:text;not null" json:"content"` // content 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&NtfTemplate{}) }
|
||||
func (table *NtfTemplate) TableName() string { return "ntf_template" }
|
||||
@@ -1,18 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Report 对应 report,保存统计报表档案。
|
||||
type Report struct {
|
||||
Entity // 公共实体字段
|
||||
ReportCode string `gorm:"column:report_code;type:varchar(64);not null;uniqueIndex" json:"report_code"` // report_code 业务字段
|
||||
ReportType string `gorm:"column:report_type;type:varchar(32);not null" json:"report_type"` // report_type 业务字段
|
||||
StatPeriod string `gorm:"column:stat_period;type:varchar(64);not null" json:"stat_period"` // stat_period 业务字段
|
||||
GeneratedAt time.Time `gorm:"column:generated_at;type:timestamptz;not null" json:"generated_at"` // generated_at 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&Report{}) }
|
||||
func (table *Report) TableName() string { return "report" }
|
||||
@@ -1,15 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// ReportItem 对应 report_item,保存报表维度明细。
|
||||
type ReportItem struct {
|
||||
Entity // 公共实体字段
|
||||
ReportID uint64 `gorm:"column:report_id;not null;index" json:"report_id"` // report_id 业务字段
|
||||
Dimension string `gorm:"column:dimension;type:varchar(128);not null" json:"dimension"` // dimension 业务字段
|
||||
MetricCode string `gorm:"column:metric_code;type:varchar(64);not null" json:"metric_code"` // metric_code 业务字段
|
||||
MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` // metric_value 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ReportItem{}) }
|
||||
func (table *ReportItem) TableName() string { return "report_item" }
|
||||
@@ -1,19 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReportMetricSnapshot 对应 report_metric_snapshot,保存指标快照。
|
||||
type ReportMetricSnapshot struct {
|
||||
Entity // 公共实体字段
|
||||
MetricCode string `gorm:"column:metric_code;type:varchar(64);not null;index" json:"metric_code"` // metric_code 业务字段
|
||||
ScopeType string `gorm:"column:scope_type;type:varchar(32);not null" json:"scope_type"` // scope_type 业务字段
|
||||
ScopeID uint64 `gorm:"column:scope_id;not null;default:0;index" json:"scope_id"` // scope_id 业务字段
|
||||
StatAt time.Time `gorm:"column:stat_at;type:timestamptz;not null;index" json:"stat_at"` // stat_at 业务字段
|
||||
MetricValue string `gorm:"column:metric_value;type:varchar(128);not null" json:"metric_value"` // metric_value 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&ReportMetricSnapshot{}) }
|
||||
func (table *ReportMetricSnapshot) TableName() string { return "report_metric_snapshot" }
|
||||
@@ -7,9 +7,9 @@ type SafeRule struct {
|
||||
Entity // 公共实体字段
|
||||
RuleCode string `gorm:"column:rule_code;type:varchar(64);not null;uniqueIndex" json:"rule_code"` // rule_code 业务字段
|
||||
VersionNo int `gorm:"column:version_no;not null;default:1" json:"version_no"` // version_no 业务字段
|
||||
Threshold string `gorm:"column:threshold;type:jsonb;not null;default:'{}'" json:"threshold"` // threshold 业务字段
|
||||
Threshold string `gorm:"column:threshold;type:text;not null;default:''" json:"threshold"` // threshold 业务字段
|
||||
Action string `gorm:"column:action;type:varchar(64);not null" json:"action"` // action 业务字段
|
||||
GrayScope string `gorm:"column:gray_scope;type:jsonb;not null;default:'{}'" json:"gray_scope"` // gray_scope 业务字段
|
||||
GrayScope string `gorm:"column:gray_scope;type:text;not null;default:''" json:"gray_scope"` // gray_scope 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&SafeRule{}) }
|
||||
|
||||
@@ -32,7 +32,6 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
|
||||
registerCommerceRoute(protected)
|
||||
registerFinanceRoute(protected)
|
||||
registerContentRoute(protected)
|
||||
registerAuditRoute(protected)
|
||||
registerPlatformRoute(protected)
|
||||
}
|
||||
|
||||
@@ -126,24 +125,13 @@ func registerFinanceRoute(group *gin.RouterGroup) {
|
||||
registerReadOnlyResource(group, "/wallet/wallet_ledger", &models.WalletLedger{})
|
||||
registerReadOnlyResource(group, "/wallet/wallet_recharge", &models.WalletRecharge{})
|
||||
registerReadOnlyResource(group, "/wallet/wallet_withdrawal", &models.WalletWithdrawal{})
|
||||
registerReadOnlyResource(group, "/report/report", &models.Report{})
|
||||
registerReadOnlyResource(group, "/report/report_item", &models.ReportItem{})
|
||||
registerReadOnlyResource(group, "/report/report_metric_snapshot", &models.ReportMetricSnapshot{})
|
||||
}
|
||||
|
||||
func registerContentRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/content/cnt_content", &models.CntContent{}, []string{"content_type", "title", "body", "version_no", "publish_status"})
|
||||
registerRestrictedWritableResource(group, "/notification/ntf_template", &models.NtfTemplate{}, []string{"template_code", "channel", "content"})
|
||||
registerRestrictedWritableResource(group, "/content/cms_content", &models.CmsContent{}, []string{"content_type", "title", "body", "version_no", "publish_status"})
|
||||
registerRestrictedWritableResource(group, "/customer_service/cs_ticket", &models.CsTicket{}, []string{"ticket_no", "category", "priority"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
}
|
||||
|
||||
func registerAuditRoute(group *gin.RouterGroup) {
|
||||
registerReadOnlyResource(group, "/audit/audit_operation_log", &models.AuditOperationLog{})
|
||||
registerReadOnlyResource(group, "/audit/audit_export_log", &models.AuditExportLog{})
|
||||
registerReadOnlyResource(group, "/audit/audit_approval", &models.AuditApproval{})
|
||||
group.POST("/audit/audit_approval/:identity/approve", platform.ApproveAudit)
|
||||
}
|
||||
|
||||
func registerWritableResource(group *gin.RouterGroup, path string, list, create, get, update gin.HandlerFunc, model any) {
|
||||
resource := group.Group(path)
|
||||
resource.GET("", list)
|
||||
|
||||
@@ -142,7 +142,7 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
|
||||
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacySafetyPrefix+"event/:identity/disposals", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
|
||||
func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T) {
|
||||
func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
|
||||
engine := gin.New()
|
||||
RegisterPlatform("heqi", engine)
|
||||
|
||||
@@ -156,7 +156,7 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
|
||||
|
||||
for _, resource := range []string{
|
||||
"/finance/fin_payment", "/finance/fin_settlement", "/finance/fin_reconciliation",
|
||||
"/content/cnt_content", "/notification/ntf_template", "/customer_service/cs_ticket",
|
||||
"/content/cms_content", "/customer_service/cs_ticket",
|
||||
} {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource+"/:identity", http.MethodGet, http.MethodPut, http.MethodDelete)
|
||||
@@ -165,8 +165,6 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
|
||||
|
||||
for _, resource := range []string{
|
||||
"/wallet/wallet", "/wallet/wallet_ledger", "/wallet/wallet_recharge", "/wallet/wallet_withdrawal",
|
||||
"/report/report", "/report/report_item", "/report/report_metric_snapshot",
|
||||
"/audit/audit_operation_log", "/audit/audit_export_log", "/audit/audit_approval",
|
||||
} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
assertRouteMethods(t, routes, path, http.MethodGet)
|
||||
@@ -178,15 +176,16 @@ func TestPlatformFinanceContentAndAuditRoutesFollowTheirContracts(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodPost)
|
||||
|
||||
legacyAuditPrefix := "/audit/" + "au" + "d_"
|
||||
for _, resource := range []string{legacyAuditPrefix + "operation_log", legacyAuditPrefix + "export_log", legacyAuditPrefix + "approval"} {
|
||||
for _, resource := range []string{
|
||||
"/content/cnt_content", "/notification/ntf_template",
|
||||
"/report/report", "/report/report_item", "/report/report_metric_snapshot",
|
||||
"/audit/audit_operation_log", "/audit/audit_export_log", "/audit/audit_approval",
|
||||
} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
assertNoRouteMethods(t, routes, path, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, path+"/:identity", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
assertNoRouteMethods(t, routes, "/heqi/platform/v1"+legacyAuditPrefix+"approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
assertNoRouteMethods(t, routes, "/heqi/platform/v1/audit/audit_approval/:identity/approve", http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
|
||||
func assertRouteMethods(t *testing.T, routes map[string]map[string]bool, path string, methods ...string) {
|
||||
|
||||
Reference in New Issue
Block a user