fix(platform): enforce identity relations
This commit is contained in:
@@ -89,6 +89,7 @@ func ExpectedResources() []ResourceContract {
|
||||
{Domain: "ec", Name: "ec_product_image", Mode: Writable, PageKind: "list"},
|
||||
{Domain: "ec", Name: "ec_cart", Mode: Writable, PageKind: "list"},
|
||||
{Domain: "ec", Name: "ec_order", Mode: Writable, PageKind: "list"},
|
||||
{Domain: "ec", Name: "ec_order_item", Mode: Writable, PageKind: "list"},
|
||||
{Domain: "ec", Name: "ec_review", Mode: Writable, PageKind: "list"},
|
||||
{Domain: "delivery", Name: "delivery_task", Mode: Writable, PageKind: "list"},
|
||||
{Domain: "delivery", Name: "delivery_track", Mode: Writable, PageKind: "list"},
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
func TestExpectedResources(t *testing.T) {
|
||||
assertContract(t, ExpectedResources(), "gas", "gas_basic", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "safety", "saf_event", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "ec", "ec_order_item", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "wallet", "wallet_ledger", ReadOnly, "list")
|
||||
}
|
||||
|
||||
@@ -183,6 +184,49 @@ func TestGetEcOrderReturnsOrderItems(t *testing.T) {
|
||||
assertMockExpectations(t, mock)
|
||||
}
|
||||
|
||||
func TestPrepareResourceValuesResolvesRequiredIdentityRelationsAndRejectsInvalidPayloads(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_order" WHERE identity = $1 ORDER BY "ec_order"."id" LIMIT $2`)).
|
||||
WithArgs("order-a", 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(uint64(1)))
|
||||
mock.ExpectQuery(regexp.QuoteMeta(`SELECT "id" FROM "ec_product" WHERE identity = $1 ORDER BY "ec_product"."id" LIMIT $2`)).
|
||||
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","quantity":2}`))
|
||||
values, err := prepareResourceValues(ctx, []string{"quantity"}, []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},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if values["ec_order_id"] != uint64(1) || values["ec_product_id"] != uint64(2) || values["quantity"] != float64(2) {
|
||||
t.Fatalf("unexpected resolved values: %#v", values)
|
||||
}
|
||||
assertMockExpectations(t, mock)
|
||||
|
||||
for _, body := range []string{`{}`, `{"ec_order_id":1}`, `{"quantity":2}`} {
|
||||
ctx, _ := updateContext(http.MethodPost, "/ec/ec_order_item", "", []byte(body))
|
||||
if _, err := prepareResourceValues(ctx, []string{"quantity"}, []ResourceRelation{{Input: "ec_order_identity", Column: "ec_order_id", Model: &models.EcOrder{}, Required: true}}); err == nil {
|
||||
t.Fatalf("payload %s was accepted", body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceResponseDoesNotExposeAutoIncrementRelationIDs(t *testing.T) {
|
||||
response := resourceResponse(map[string]any{"identity": "item-a", "id": uint64(1), "ec_order_id": uint64(2), "ec_product_id": uint64(3), "items": []any{map[string]any{"identity": "child-a", "delivery_task_id": uint64(4)}}})
|
||||
encoded, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, forbidden := range []string{`"id"`, `"ec_order_id"`, `"ec_product_id"`, `"delivery_task_id"`} {
|
||||
if strings.Contains(string(encoded), forbidden) {
|
||||
t.Fatalf("response exposed internal relation key %s: %s", forbidden, encoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDeliveryTrackOrdersAndMasksPointsWithoutPreciseLocationScope(t *testing.T) {
|
||||
_, mock := setupPlatformRoleDatabase(t)
|
||||
now := time.Now().UTC()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
@@ -14,13 +15,22 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ResourceRelation accepts a stable external identity while retaining the
|
||||
// relation's database ID as an internal persistence detail.
|
||||
type ResourceRelation struct {
|
||||
Input string
|
||||
Column string
|
||||
Model any
|
||||
Required bool
|
||||
}
|
||||
|
||||
// ResourceHandlers supplies the common identity-based CRUD boundary used by
|
||||
// platform resources whose writable fields are explicitly declared by routes.
|
||||
func ResourceHandlers(model any, createFields, updateFields []string) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
func ResourceHandlers(model any, createFields, updateFields []string, relations ...ResourceRelation) (gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc, gin.HandlerFunc) {
|
||||
return func(ctx *gin.Context) { listResource(ctx, model) },
|
||||
func(ctx *gin.Context) { createResource(ctx, model, createFields) },
|
||||
func(ctx *gin.Context) { createResource(ctx, model, createFields, relations) },
|
||||
func(ctx *gin.Context) { getResource(ctx, model) },
|
||||
func(ctx *gin.Context) { updateResource(ctx, model, updateFields) }
|
||||
func(ctx *gin.Context) { updateResource(ctx, model, updateFields, relations) }
|
||||
}
|
||||
|
||||
func listResource(ctx *gin.Context, model any) {
|
||||
@@ -36,7 +46,7 @@ func listResource(ctx *gin.Context, model any) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": list.Elem().Interface()})
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": resourceResponse(list.Elem().Interface())})
|
||||
}
|
||||
|
||||
func getResource(ctx *gin.Context, model any) {
|
||||
@@ -45,16 +55,15 @@ func getResource(ctx *gin.Context, model any) {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, data.Interface())
|
||||
infra.Response.Success(ctx, resourceResponse(data.Interface()))
|
||||
}
|
||||
|
||||
func createResource(ctx *gin.Context, model any, allowedFields []string) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
func createResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
|
||||
values, err := prepareResourceValues(ctx, allowedFields, relations)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
values := filterFields(input, allowedFields)
|
||||
encoded, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
@@ -70,16 +79,100 @@ func createResource(ctx *gin.Context, model any, allowedFields []string) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, data.Interface())
|
||||
infra.Response.Success(ctx, resourceResponse(data.Interface()))
|
||||
}
|
||||
|
||||
func updateResource(ctx *gin.Context, model any, allowedFields []string) {
|
||||
func updateResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, model, input, allowedFields)
|
||||
values, err := resolveResourceRelations(input, allowedFields, relations, false)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
if len(values) == 0 {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, model, values, append(allowedFields, relationColumns(relations)...))
|
||||
}
|
||||
|
||||
func prepareResourceValues(ctx *gin.Context, allowedFields []string, relations []ResourceRelation) (map[string]any, error) {
|
||||
var input map[string]any
|
||||
if err := ctx.ShouldBindJSON(&input); err != nil || len(input) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
values, err := resolveResourceRelations(input, allowedFields, relations, true)
|
||||
if err != nil || len(values) == 0 {
|
||||
return nil, errors.New("invalid resource payload")
|
||||
}
|
||||
return values, 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 {
|
||||
raw, exists := input[relation.Input]
|
||||
if !exists {
|
||||
if requireRelations && relation.Required {
|
||||
return nil, errors.New("missing required relation")
|
||||
}
|
||||
continue
|
||||
}
|
||||
identity, ok := raw.(string)
|
||||
if !ok || strings.TrimSpace(identity) == "" {
|
||||
return nil, errors.New("invalid relation identity")
|
||||
}
|
||||
var related struct{ ID uint64 }
|
||||
if err := impl.DBService.Model(relation.Model).Select("id").Where("identity = ?", identity).First(&related).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values[relation.Column] = related.ID
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func relationColumns(relations []ResourceRelation) []string {
|
||||
columns := make([]string, 0, len(relations))
|
||||
for _, relation := range relations {
|
||||
columns = append(columns, relation.Column)
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
// resourceResponse strips database surrogate IDs from API data. Business
|
||||
// identities are the only public relation keys accepted or returned.
|
||||
func resourceResponse(value any) any {
|
||||
encoded, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return value
|
||||
}
|
||||
var decoded any
|
||||
if err := json.Unmarshal(encoded, &decoded); err != nil {
|
||||
return value
|
||||
}
|
||||
return stripInternalIDs(decoded)
|
||||
}
|
||||
|
||||
func stripInternalIDs(value any) any {
|
||||
switch data := value.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range data {
|
||||
if key == "id" || strings.HasSuffix(key, "_id") {
|
||||
delete(data, key)
|
||||
continue
|
||||
}
|
||||
data[key] = stripInternalIDs(item)
|
||||
}
|
||||
case []any:
|
||||
for index := range data {
|
||||
data[index] = stripInternalIDs(data[index])
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// DisposeSafetyEvent atomically updates an event and appends its operator-owned
|
||||
@@ -130,7 +223,7 @@ func DisposeSafetyEvent(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, disposal)
|
||||
infra.Response.Success(ctx, resourceResponse(disposal))
|
||||
}
|
||||
|
||||
// GetEcOrder returns the order together with its immutable item snapshots.
|
||||
@@ -145,7 +238,7 @@ func GetEcOrder(ctx *gin.Context) {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"order": order, "items": items})
|
||||
infra.Response.Success(ctx, resourceResponse(gin.H{"order": order, "items": items}))
|
||||
}
|
||||
|
||||
// GetDeliveryTrack returns time-ordered points. Precise coordinates are only
|
||||
@@ -171,5 +264,5 @@ func GetDeliveryTrack(ctx *gin.Context) {
|
||||
points[index].Latitude = ""
|
||||
}
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"track": track, "points": points})
|
||||
infra.Response.Success(ctx, resourceResponse(gin.H{"track": track, "points": points}))
|
||||
}
|
||||
|
||||
@@ -40,15 +40,17 @@ func registerGasRoute(group *gin.RouterGroup) {
|
||||
func registerDeliveryRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/delivery/delivery_basic", platform.ListDeliveryBasic, platform.CreateDeliveryBasic, platform.GetDeliveryBasic, platform.UpdateDeliveryBasic, &models.DeliveryBasic{})
|
||||
registerWritableResource(group, "/delivery/delivery_account", platform.ListDeliveryAccount, platform.CreateDeliveryAccount, platform.GetDeliveryAccount, platform.UpdateDeliveryAccount, &models.DeliveryAccount{})
|
||||
registerRestrictedWritableResource(group, "/delivery/delivery_task", &models.DeliveryTask{}, []string{"ec_order_id", "staff_account_id", "delivery_point_id"})
|
||||
list, create, _, update := platform.ResourceHandlers(&models.DeliveryTrack{}, []string{"delivery_task_id", "started_at", "completed_at"}, []string{"delivery_task_id", "started_at", "completed_at"})
|
||||
registerRestrictedWritableResource(group, "/delivery/delivery_task", &models.DeliveryTask{}, nil,
|
||||
requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), optionalRelation("staff_account_identity", "staff_account_id", &models.StaffAccount{}), requiredRelation("delivery_basic_identity", "delivery_point_id", &models.DeliveryBasic{}))
|
||||
trackRelations := []platform.ResourceRelation{requiredRelation("delivery_task_identity", "delivery_task_id", &models.DeliveryTask{})}
|
||||
list, create, _, update := platform.ResourceHandlers(&models.DeliveryTrack{}, []string{"started_at", "completed_at"}, []string{"started_at", "completed_at"}, trackRelations...)
|
||||
registerWritableResource(group, "/delivery/delivery_track", list, create, platform.GetDeliveryTrack, update, &models.DeliveryTrack{})
|
||||
registerRestrictedWritableResource(group, "/delivery/delivery_track_point", &models.DeliveryTrackPoint{}, []string{"delivery_track_id", "point_type", "occurred_at", "longitude", "latitude"})
|
||||
registerRestrictedWritableResource(group, "/delivery/delivery_track_point", &models.DeliveryTrackPoint{}, []string{"point_type", "occurred_at", "longitude", "latitude"}, requiredRelation("delivery_track_identity", "delivery_track_id", &models.DeliveryTrack{}))
|
||||
}
|
||||
|
||||
func registerDeviceRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/device/dev_smart_cylinder_valve", &models.DevSmartCylinderValve{}, []string{"device_no", "model", "online_status", "owner_identity"})
|
||||
registerRestrictedWritableResource(group, "/device/dev_device_binding", &models.DevDeviceBinding{}, []string{"smart_cylinder_valve_id", "user_account_id", "effective_at", "expired_at"})
|
||||
registerRestrictedWritableResource(group, "/device/dev_device_binding", &models.DevDeviceBinding{}, []string{"effective_at", "expired_at"}, requiredRelation("smart_cylinder_valve_identity", "smart_cylinder_valve_id", &models.DevSmartCylinderValve{}), requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
list, _, get, _ := platform.ResourceHandlers(&models.DevTelemetry{}, nil, nil)
|
||||
telemetry := group.Group("/device/dev_telemetry")
|
||||
telemetry.GET("", list)
|
||||
@@ -58,19 +60,21 @@ func registerDeviceRoute(group *gin.RouterGroup) {
|
||||
func registerSafetyRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/safety/saf_rule", &models.SafRule{}, []string{"rule_code", "version_no", "threshold", "action", "gray_scope"})
|
||||
registerRestrictedWritableResource(group, "/safety/saf_event", &models.SafEvent{}, []string{"event_code", "level", "title", "smart_cylinder_valve_identity", "sla_at"})
|
||||
registerRestrictedWritableResource(group, "/safety/saf_inspection", &models.SafInspection{}, []string{"user_account_id", "staff_account_id", "result", "evidence_uri"})
|
||||
registerRestrictedWritableResource(group, "/safety/saf_inspection", &models.SafInspection{}, []string{"result", "evidence_uri"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("staff_account_identity", "staff_account_id", &models.StaffAccount{}))
|
||||
group.POST("/safety/saf_event/:identity/disposals", platform.DisposeSafetyEvent)
|
||||
}
|
||||
|
||||
func registerCommerceRoute(group *gin.RouterGroup) {
|
||||
registerRestrictedWritableResource(group, "/ec/ec_category", &models.EcCategory{}, []string{"parent_id", "name", "sort_no"})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product", &models.EcProduct{}, []string{"ec_category_id", "product_code", "name", "price_amount", "stock_quantity"})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product_attribute", &models.EcProductAttribute{}, []string{"ec_product_id", "name", "value", "sort_no"})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product_image", &models.EcProductImage{}, []string{"ec_product_id", "image_uri", "sort_no", "is_cover"})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_cart", &models.EcCart{}, []string{"user_account_id", "ec_product_id", "quantity", "selected"})
|
||||
list, create, _, update := platform.ResourceHandlers(&models.EcOrder{}, []string{"order_no", "user_account_id", "gas_station_id", "delivery_point_id", "total_amount"}, []string{"gas_station_id", "delivery_point_id", "total_amount"})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_category", &models.EcCategory{}, []string{"name", "sort_no"}, optionalRelation("parent_identity", "parent_id", &models.EcCategory{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product", &models.EcProduct{}, []string{"product_code", "name", "price_amount", "stock_quantity"}, requiredRelation("ec_category_identity", "ec_category_id", &models.EcCategory{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product_attribute", &models.EcProductAttribute{}, []string{"name", "value", "sort_no"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_product_image", &models.EcProductImage{}, []string{"image_uri", "sort_no", "is_cover"}, requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_cart", &models.EcCart{}, []string{"quantity", "selected"}, requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
orderRelations := []platform.ResourceRelation{requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}), optionalRelation("gas_basic_identity", "gas_station_id", &models.GasBasic{}), optionalRelation("delivery_basic_identity", "delivery_point_id", &models.DeliveryBasic{})}
|
||||
list, create, _, update := platform.ResourceHandlers(&models.EcOrder{}, []string{"order_no", "total_amount"}, []string{"total_amount"}, orderRelations...)
|
||||
registerWritableResource(group, "/ec/ec_order", list, create, platform.GetEcOrder, update, &models.EcOrder{})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_review", &models.EcReview{}, []string{"ec_order_id", "ec_product_id", "user_account_id", "score", "content"})
|
||||
registerRestrictedWritableResource(group, "/ec/ec_order_item", &models.EcOrderItem{}, []string{"product_snapshot", "quantity", "sale_amount"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}))
|
||||
registerRestrictedWritableResource(group, "/ec/ec_review", &models.EcReview{}, []string{"score", "content"}, requiredRelation("ec_order_identity", "ec_order_id", &models.EcOrder{}), requiredRelation("ec_product_identity", "ec_product_id", &models.EcProduct{}), requiredRelation("user_account_identity", "user_account_id", &models.UserAccount{}))
|
||||
}
|
||||
|
||||
func registerStaffRoute(group *gin.RouterGroup) {
|
||||
@@ -107,7 +111,15 @@ func registerWritableResource(group *gin.RouterGroup, path string, list, create,
|
||||
resource.DELETE("/:identity", func(ctx *gin.Context) { platform.ArchiveRecord(ctx, model) })
|
||||
}
|
||||
|
||||
func registerRestrictedWritableResource(group *gin.RouterGroup, path string, model any, fields []string) {
|
||||
list, create, get, update := platform.ResourceHandlers(model, fields, fields)
|
||||
func registerRestrictedWritableResource(group *gin.RouterGroup, path string, model any, fields []string, relations ...platform.ResourceRelation) {
|
||||
list, create, get, update := platform.ResourceHandlers(model, fields, fields, relations...)
|
||||
registerWritableResource(group, path, list, create, get, update, model)
|
||||
}
|
||||
|
||||
func requiredRelation(input, column string, model any) platform.ResourceRelation {
|
||||
return platform.ResourceRelation{Input: input, Column: column, Model: model, Required: true}
|
||||
}
|
||||
|
||||
func optionalRelation(input, column string, model any) platform.ResourceRelation {
|
||||
return platform.ResourceRelation{Input: input, Column: column, Model: model}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestPlatformDeviceSafetyCommerceAndDeliveryRoutesFollowTheirContracts(t *te
|
||||
for _, resource := range []string{
|
||||
"/device/dev_smart_cylinder_valve", "/device/dev_device_binding",
|
||||
"/safety/saf_rule", "/safety/saf_event", "/safety/saf_inspection",
|
||||
"/ec/ec_category", "/ec/ec_product", "/ec/ec_product_attribute", "/ec/ec_product_image", "/ec/ec_cart", "/ec/ec_order", "/ec/ec_review",
|
||||
"/ec/ec_category", "/ec/ec_product", "/ec/ec_product_attribute", "/ec/ec_product_image", "/ec/ec_cart", "/ec/ec_order", "/ec/ec_order_item", "/ec/ec_review",
|
||||
"/delivery/delivery_task", "/delivery/delivery_track", "/delivery/delivery_track_point",
|
||||
} {
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1"+resource, http.MethodGet, http.MethodPost)
|
||||
|
||||
Reference in New Issue
Block a user