2026-07-27 01:11:38 +08:00
package platform
2026-07-27 01:32:44 +08:00
import (
2026-07-27 01:49:02 +08:00
"bytes"
"encoding/json"
"errors"
2026-07-27 01:32:44 +08:00
"net/http"
2026-07-27 01:49:02 +08:00
"net/http/httptest"
2026-07-27 12:19:34 +08:00
"reflect"
2026-07-27 01:49:02 +08:00
"regexp"
2026-07-27 02:47:39 +08:00
"strings"
2026-07-27 01:32:44 +08:00
"testing"
2026-07-27 02:47:39 +08:00
"time"
2026-07-27 01:49:02 +08:00
"git.apinb.com/bsm-sdk/core/errcode"
2026-07-27 02:47:39 +08:00
"git.apinb.com/bsm-sdk/core/types"
2026-07-27 01:49:02 +08:00
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/status"
"gorm.io/driver/postgres"
"gorm.io/gorm"
2026-07-27 01:32:44 +08:00
)
2026-07-27 01:11:38 +08:00
func TestExpectedResources ( t * testing . T ) {
assertContract ( t , ExpectedResources ( ) , "gas" , "gas_basic" , Writable , "list" )
2026-07-27 02:59:28 +08:00
assertContract ( t , ExpectedResources ( ) , "ec" , "ec_order_item" , Writable , "list" )
2026-07-27 01:11:38 +08:00
assertContract ( t , ExpectedResources ( ) , "wallet" , "wallet_ledger" , ReadOnly , "list" )
2026-07-27 12:02:08 +08:00
assertContract ( t , ExpectedResources ( ) , "delivery" , "delivery_track_point" , ReadOnly , "list" )
2026-07-27 01:11:38 +08:00
}
2026-07-27 14:03:45 +08:00
func TestSafeAndAuditResourceContracts ( t * testing . T ) {
2026-07-27 14:52:58 +08:00
legacySafetyPrefix := "sa" + "f_"
legacyAuditPrefix := "au" + "d_"
2026-07-27 14:03:45 +08:00
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" )
2026-07-27 14:16:39 +08:00
for _ , contract := range ExpectedResources ( ) {
2026-07-27 14:52:58 +08:00
if ( contract . Domain == "safety" && strings . HasPrefix ( contract . Name , legacySafetyPrefix ) ) ||
( contract . Domain == "audit" && strings . HasPrefix ( contract . Name , legacyAuditPrefix ) ) {
2026-07-27 14:16:39 +08:00
t . Fatalf ( "legacy resource contract %s/%s must not be registered" , contract . Domain , contract . Name )
}
}
2026-07-27 14:03:45 +08:00
}
2026-07-27 01:32:44 +08:00
func TestResourceDefinitionAllowsOnlySupportedMethods ( t * testing . T ) {
if ( ResourceDefinition { Mode : ReadOnly } ) . Allows ( http . MethodPost ) {
t . Fatal ( "readonly allows POST" )
}
if ! ( ResourceDefinition { Mode : AppendOnly } ) . Allows ( http . MethodPost ) {
t . Fatal ( "append-only rejects POST" )
}
if ( ResourceDefinition { Mode : AppendOnly } ) . Allows ( http . MethodDelete ) {
t . Fatal ( "append-only allows DELETE" )
}
}
func TestArchiveValuesOnlyArchives ( t * testing . T ) {
got := archiveValues ( )
if len ( got ) != 1 || got [ "status" ] != "archived" {
t . Fatalf ( "unexpected archive values: %#v" , got )
}
}
func TestFilterFieldsKeepsOnlyAllowedKeys ( t * testing . T ) {
got := filterFields ( map [ string ] any { "name" : "n" , "password_hash" : "x" } , [ ] string { "name" } )
if len ( got ) != 1 || got [ "name" ] != "n" {
t . Fatalf ( "unexpected filtered fields: %#v" , got )
}
}
2026-07-27 01:49:02 +08:00
func TestResourceDefinitionAllowsOnlyMethodsForEachMode ( t * testing . T ) {
tests := [ ] struct {
name string
mode ResourceMode
method string
want bool
} {
{ "readonly GET" , ReadOnly , http . MethodGet , true } ,
{ "readonly POST" , ReadOnly , http . MethodPost , false } ,
{ "append-only GET" , AppendOnly , http . MethodGet , true } ,
{ "append-only POST" , AppendOnly , http . MethodPost , true } ,
{ "append-only PUT" , AppendOnly , http . MethodPut , false } ,
{ "writable GET" , Writable , http . MethodGet , true } ,
{ "writable POST" , Writable , http . MethodPost , true } ,
{ "writable PUT" , Writable , http . MethodPut , true } ,
{ "writable PATCH" , Writable , http . MethodPatch , true } ,
{ "writable DELETE" , Writable , http . MethodDelete , true } ,
{ "writable OPTIONS" , Writable , http . MethodOptions , false } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
if got := ( ResourceDefinition { Mode : test . mode } ) . Allows ( test . method ) ; got != test . want {
t . Fatalf ( "Allows(%s) = %t, want %t" , test . method , got , test . want )
}
} )
}
}
func TestUpdateAllowedByIdentityFiltersUnknownFieldsAndUsesIdentity ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectBegin ( )
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3 ` ) ) .
WithArgs ( "disabled" , sqlmock . AnyArg ( ) , "role-a" ) .
WillReturnResult ( sqlmock . NewResult ( 0 , 1 ) )
mock . ExpectCommit ( )
ctx , recorder := updateContext ( http . MethodPatch , "/roles/role-a" , "role-a" , nil )
updateAllowedByIdentity ( ctx , & models . PlatformRole { } , gin . H { "status" : "disabled" , "is_system" : true } , [ ] string { "status" } )
assertResponseCode ( t , recorder , 0 )
assertMockExpectations ( t , mock )
}
func TestUpdateAllowedByIdentityReturnsNotFoundForZeroRows ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectBegin ( )
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3 ` ) ) .
WithArgs ( "disabled" , sqlmock . AnyArg ( ) , "missing" ) .
WillReturnResult ( sqlmock . NewResult ( 0 , 0 ) )
mock . ExpectCommit ( )
ctx , recorder := updateContext ( http . MethodPatch , "/roles/missing" , "missing" , nil )
updateAllowedByIdentity ( ctx , & models . PlatformRole { } , gin . H { "status" : "disabled" } , [ ] string { "status" } )
assertResponseCode ( t , recorder , int32 ( status . Code ( errcode . ErrRecordNotFound ) ) )
assertMockExpectations ( t , mock )
}
func TestUpdateAllowedByIdentityReturnsUniformResponseForDatabaseError ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectBegin ( )
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3 ` ) ) .
WithArgs ( "disabled" , sqlmock . AnyArg ( ) , "role-a" ) .
WillReturnError ( errors . New ( "database unavailable" ) )
mock . ExpectRollback ( )
ctx , recorder := updateContext ( http . MethodPatch , "/roles/role-a" , "role-a" , nil )
updateAllowedByIdentity ( ctx , & models . PlatformRole { } , gin . H { "status" : "disabled" } , [ ] string { "status" } )
var reply responseBody
if err := json . Unmarshal ( recorder . Body . Bytes ( ) , & reply ) ; err != nil {
t . Fatal ( err )
}
if reply . Code != 500 || reply . Message == "" || string ( reply . Details ) != ` "" ` {
t . Fatalf ( "database error did not use uniform response: %s" , recorder . Body . String ( ) )
}
}
func TestPlatformRoleStatusAndArchiveReturnNotFoundWhenUpdateAffectsZeroRows ( t * testing . T ) {
for _ , test := range [ ] struct {
name string
handler gin . HandlerFunc
method string
body string
status string
} {
{ "status" , UpdatePlatformRoleStatus , http . MethodPatch , ` { "status":"disabled"} ` , "disabled" } ,
{ "archive" , ArchivePlatformRole , http . MethodDelete , "" , "archived" } ,
} {
t . Run ( test . name , func ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2 ` ) ) .
WithArgs ( "role-a" , 1 ) .
WillReturnRows ( platformRoleRows ( "role-a" ) )
mock . ExpectBegin ( )
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "platform_role" SET "status"=$1,"updated_at"=$2 WHERE identity = $3 ` ) ) .
WithArgs ( test . status , sqlmock . AnyArg ( ) , "role-a" ) .
WillReturnResult ( sqlmock . NewResult ( 0 , 0 ) )
mock . ExpectCommit ( )
ctx , recorder := updateContext ( test . method , "/roles/role-a" , "role-a" , [ ] byte ( test . body ) )
2026-07-27 13:12:43 +08:00
ctx . Set ( "Auth" , & types . JwtClaims { Role : "root" } )
2026-07-27 01:49:02 +08:00
test . handler ( ctx )
assertResponseCode ( t , recorder , int32 ( status . Code ( errcode . ErrRecordNotFound ) ) )
assertMockExpectations ( t , mock )
} )
}
}
2026-07-27 02:47:39 +08:00
func TestGetEcOrderReturnsOrderItems ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "ec_order" WHERE identity = $1 ORDER BY "ec_order"."id" LIMIT $2 ` ) ) .
WithArgs ( "order-a" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "order_no" , "user_account_id" , "gas_station_id" , "delivery_point_id" , "total_amount" } ) .
AddRow ( uint64 ( 1 ) , "order-a" , nil , nil , "enabled" , 1 , "O-1" , uint64 ( 2 ) , uint64 ( 3 ) , uint64 ( 4 ) , int64 ( 500 ) ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "ec_order_item" WHERE ec_order_id = $1 ORDER BY id asc ` ) ) .
WithArgs ( uint64 ( 1 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "ec_order_id" , "ec_product_id" , "product_snapshot" , "quantity" , "sale_amount" } ) .
AddRow ( uint64 ( 2 ) , "item-a" , nil , nil , "enabled" , 1 , uint64 ( 1 ) , uint64 ( 5 ) , ` { } ` , 2 , int64 ( 500 ) ) )
2026-07-27 10:42:16 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "delivery_basic" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 4 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 4 ) , "delivery-a" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "ec_order" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 1 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 1 ) , "order-a" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "ec_product" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 5 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 5 ) , "product-a" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 3 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 3 ) , "gas-a" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "user_account" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 2 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 2 ) , "user-a" ) )
2026-07-27 02:47:39 +08:00
ctx , recorder := updateContext ( http . MethodGet , "/ec/ec_order/order-a" , "order-a" , nil )
GetEcOrder ( ctx )
assertResponseCode ( t , recorder , 0 )
2026-07-27 10:42:16 +08:00
if ! strings . Contains ( recorder . Body . String ( ) , ` "items" ` ) || ! strings . Contains ( recorder . Body . String ( ) , ` "item-a" ` ) || strings . Contains ( recorder . Body . String ( ) , ` _id" ` ) {
2026-07-27 02:47:39 +08:00
t . Fatalf ( "order detail omitted its items: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 02:59:28 +08:00
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} ` ) )
2026-07-27 12:19:34 +08:00
values , err := prepareResourceValues ( ctx , & models . EcOrderItem { } , [ ] string { "quantity" } , [ ] ResourceRelation {
2026-07-27 02:59:28 +08:00
{ 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 ) )
2026-07-27 12:19:34 +08:00
if _ , err := prepareResourceValues ( ctx , & models . EcOrderItem { } , [ ] string { "quantity" } , [ ] ResourceRelation { { Input : "ec_order_identity" , Column : "ec_order_id" , Model : & models . EcOrder { } , Required : true } } ) ; err == nil {
2026-07-27 02:59:28 +08:00
t . Fatalf ( "payload %s was accepted" , body )
}
}
}
2026-07-27 12:19:34 +08:00
func TestPrepareResourceValuesNormalizesStringJSONBFields ( t * testing . T ) {
t . Run ( "safety rule" , func ( t * testing . T ) {
2026-07-27 14:28:23 +08:00
ctx , _ := updateContext ( http . MethodPost , "/safety/safe_rule" , "" , [ ] byte ( ` { "rule_code":"pressure-limit","threshold": { "max":10},"action":"close-valve","gray_scope":["north"]} ` ) )
values , err := prepareResourceValues ( ctx , & models . SafeRule { } , [ ] string { "rule_code" , "threshold" , "action" , "gray_scope" } , nil )
2026-07-27 12:19:34 +08:00
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 )
}
} )
t . Run ( "order item" , func ( 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","product_snapshot": { "name":"液化气"},"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 } ,
} )
if err != nil {
t . Fatal ( err )
}
if values [ "product_snapshot" ] != ` { "name":"液化气"} ` {
t . Fatalf ( "product snapshot was not normalized to a string: %#v" , values )
}
assertMockExpectations ( t , mock )
} )
t . Run ( "invalid json string" , func ( t * testing . T ) {
2026-07-27 14:28:23 +08:00
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 {
2026-07-27 12:19:34 +08:00
t . Fatal ( "invalid JSON string was accepted for a string/jsonb field" )
}
} )
}
2026-07-27 10:11:07 +08:00
func TestCreateGasAccountResolvesGasBasicIdentityBeforePersisting ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id" FROM "gas_basic" WHERE identity = $1 ORDER BY "gas_basic"."id" LIMIT $2 ` ) ) .
WithArgs ( "gas-a" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" } ) . AddRow ( uint64 ( 8 ) ) )
mock . ExpectBegin ( )
mock . ExpectQuery ( ` INSERT INTO "gas_account" ` ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" } ) . AddRow ( uint64 ( 1 ) ) )
mock . ExpectCommit ( )
2026-07-27 12:02:08 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 8 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 8 ) , "gas-a" ) )
2026-07-27 10:11:07 +08:00
ctx , recorder := updateContext ( http . MethodPost , "/gas/gas_account" , "" , [ ] byte ( ` { "username":"operator","password":"password-123","gas_basic_identity":"gas-a"} ` ) )
CreateGasAccount ( ctx )
2026-07-27 12:02:08 +08:00
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if ! strings . Contains ( body , ` "identity" ` ) || ! strings . Contains ( body , ` "gas_basic_identity":"gas-a" ` ) {
t . Fatalf ( "create response omitted public identities: %s" , body )
}
if strings . Contains ( body , ` "gas_basic_id" ` ) || strings . Contains ( body , ` "password_hash" ` ) {
t . Fatalf ( "create response exposed internal or sensitive fields: %s" , body )
}
assertMockExpectations ( t , mock )
}
func TestListGasAccountAppliesKeywordToCountAndRows ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
keywordWhere := ` WHERE (LOWER("username") LIKE $1 OR LOWER("display_name") LIKE $2 OR LOWER("role_code") LIKE $3) `
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "gas_account" ` + keywordWhere ) ) .
WithArgs ( "%operator%" , "%operator%" , "%operator%" ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 0 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "gas_account" ` + keywordWhere + ` ORDER BY created_at desc LIMIT $4 ` ) ) .
WithArgs ( "%operator%" , "%operator%" , "%operator%" , 20 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "gas_basic_id" , "username" , "display_name" , "password_hash" , "role_code" } ) )
ctx , recorder := updateContext ( http . MethodGet , "/gas/gas_account?keyword=Operator" , "" , nil )
ListGasAccount ( ctx )
2026-07-27 10:11:07 +08:00
assertResponseCode ( t , recorder , 0 )
assertMockExpectations ( t , mock )
}
2026-07-27 12:19:34 +08:00
func TestKeywordColumnsUseSafeTextAllowlist ( t * testing . T ) {
tests := [ ] struct {
name string
model any
want [ ] string
} {
2026-07-27 14:28:23 +08:00
{ "safety rule excludes jsonb" , & models . SafeRule { } , [ ] string { "rule_code" , "action" } } ,
2026-07-27 12:19:34 +08:00
{ "gas basic excludes sensitive fields" , & models . GasBasic { } , [ ] string { "code" , "name" } } ,
{ "user address has no searchable safe text" , & models . UserAddress { } , [ ] string { } } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
got := keywordColumns ( test . model )
if ! reflect . DeepEqual ( got , test . want ) {
t . Fatalf ( "keywordColumns(%T) = %#v, want %#v" , test . model , got , test . want )
}
} )
}
}
2026-07-27 10:11:07 +08:00
func TestListPlatformMenuReturnsParentIdentityWithoutParentID ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platform_menu" ORDER BY sort_no asc, id asc ` ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "parent_id" , "menu_code" , "name" , "icon" , "path" , "sort_no" } ) .
AddRow ( uint64 ( 1 ) , "root-a" , nil , nil , "enabled" , 1 , uint64 ( 0 ) , "root" , "Root" , "" , "" , 1 ) .
AddRow ( uint64 ( 2 ) , "child-a" , nil , nil , "enabled" , 1 , uint64 ( 1 ) , "child" , "Child" , "" , "/child" , 2 ) )
ctx , recorder := updateContext ( http . MethodGet , "/platform/platform_menu" , "" , nil )
2026-07-27 12:48:35 +08:00
ctx . Set ( "Auth" , & types . JwtClaims { Role : "root" } )
2026-07-27 10:11:07 +08:00
ListPlatformMenu ( ctx )
assertResponseCode ( t , recorder , 0 )
if ! strings . Contains ( recorder . Body . String ( ) , ` "parent_identity":"root-a" ` ) || strings . Contains ( recorder . Body . String ( ) , ` "parent_id" ` ) {
t . Fatalf ( "menu response leaked parent_id or omitted identity: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 12:48:35 +08:00
func TestListPlatformMenuReturnsOnlyMenusAssignedToNonRootRole ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platform_role" WHERE role_code = $1 AND status = $2 ORDER BY "platform_role"."id" LIMIT $3 ` ) ) .
WithArgs ( "finance_operator" , "enabled" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "role_code" , "name" , "data_scope" , "is_system" } ) .
AddRow ( uint64 ( 7 ) , "finance-role" , nil , nil , "enabled" , 1 , "finance_operator" , "Finance" , "global" , false ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT platform_menu.* FROM "platform_menu" JOIN platform_role_menu_relation ON platform_role_menu_relation.platform_menu_id = platform_menu.id WHERE platform_role_menu_relation.platform_role_id = $1 AND platform_menu.status = $2 ORDER BY sort_no asc, id asc ` ) ) .
WithArgs ( uint64 ( 7 ) , "enabled" ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "parent_id" , "menu_code" , "name" , "icon" , "path" , "sort_no" } ) .
AddRow ( uint64 ( 9 ) , "finance-menu" , nil , nil , "enabled" , 1 , uint64 ( 0 ) , "finance" , "Finance" , "" , "/finance/payment" , 1 ) )
ctx , recorder := updateContext ( http . MethodGet , "/platform/platform_menu" , "" , nil )
ctx . Set ( "Auth" , & types . JwtClaims { Role : "finance_operator" } )
ListPlatformMenu ( ctx )
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if ! strings . Contains ( body , ` "identity":"finance-menu" ` ) || strings . Contains ( body , ` "gas-menu" ` ) {
t . Fatalf ( "non-root menu response was not constrained: %s" , body )
}
assertMockExpectations ( t , mock )
}
2026-07-27 02:59:28 +08:00
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 )
}
}
}
2026-07-27 12:19:34 +08:00
func TestCreatedResourceResponseUsesSafeAllowlist ( t * testing . T ) {
2026-07-27 12:02:08 +08:00
response := maskCreatedSensitiveFields ( map [ string ] any {
2026-07-27 12:19:34 +08:00
"identity" : "address-a" ,
"user_account_identity" : "user-a" ,
"status" : "draft" ,
"version" : float64 ( 1 ) ,
"phone" : "13800138000" ,
"real_name" : "张三" ,
"credential_no" : "CERT-123456" ,
"address" : "敏感详细地址" ,
"principal" : "负责人" ,
"credit_code" : "CREDIT-123" ,
"longitude" : "120.123456" ,
"latitude" : "30.456789" ,
2026-07-27 12:02:08 +08:00
} )
encoded , err := json . Marshal ( response )
if err != nil {
t . Fatal ( err )
}
body := string ( encoded )
2026-07-27 12:19:34 +08:00
if ! strings . Contains ( body , ` "identity":"address-a" ` ) || ! strings . Contains ( body , ` "user_account_identity":"user-a" ` ) || ! strings . Contains ( body , ` "status":"draft" ` ) {
t . Fatalf ( "created response omitted safe public fields: %s" , body )
2026-07-27 12:02:08 +08:00
}
2026-07-27 12:19:34 +08:00
for _ , forbidden := range [ ] string { ` "phone" ` , ` "phone_masked" ` , ` "real_name" ` , ` "credential_no" ` , ` "address" ` , ` "principal" ` , ` "credit_code" ` , ` "longitude" ` , ` "latitude" ` , "敏感详细地址" , "负责人" , "CREDIT-123" , "120.123456" , "30.456789" } {
2026-07-27 12:02:08 +08:00
if strings . Contains ( body , forbidden ) {
t . Fatalf ( "created response exposed sensitive field %s: %s" , forbidden , body )
}
}
}
2026-07-27 12:48:35 +08:00
func TestDefaultResourceResponseMasksPIIAndCoordinates ( t * testing . T ) {
ctx , _ := updateContext ( http . MethodGet , "/user/account/user-a" , "user-a" , nil )
response := protectPreciseLocation ( ctx , & models . UserAccount { } , map [ string ] any {
"identity" : "user-a" ,
"name" : "张三" ,
"phone" : "13800138000" ,
"avatar" : "https://private.example/avatar.png" ,
"address" : "敏感详细地址" ,
"longitude" : "120.123456" ,
"latitude" : "30.456789" ,
} )
encoded , err := json . Marshal ( response )
if err != nil {
t . Fatal ( err )
}
body := string ( encoded )
if ! strings . Contains ( body , ` "identity":"user-a" ` ) || ! strings . Contains ( body , ` "phone_masked":"138****8000" ` ) || ! strings . Contains ( body , ` "name_masked" ` ) {
t . Fatalf ( "default response omitted safe identity or masked PII: %s" , body )
}
for _ , forbidden := range [ ] string { ` "phone": ` , ` "name": ` , ` "avatar": ` , ` "address": ` , ` "longitude":"120.123456" ` , ` "latitude":"30.456789" ` , "张三" , "敏感详细地址" , "private.example" } {
if strings . Contains ( body , forbidden ) {
t . Fatalf ( "default response leaked %s: %s" , forbidden , body )
}
}
}
func TestExplicitPreciseScopeRetainsCoordinatesButStillMasksPII ( t * testing . T ) {
ctx , _ := updateContext ( http . MethodGet , "/user/address/address-a" , "address-a" , nil )
ctx . Set ( "Auth" , & types . JwtClaims { Extend : map [ string ] string { "location_scope" : "precise" } } )
response := protectPreciseLocation ( ctx , & models . UserAddress { } , map [ string ] any {
"identity" : "address-a" ,
"address" : "敏感详细地址" ,
"longitude" : "120.123456" ,
"latitude" : "30.456789" ,
} )
encoded , err := json . Marshal ( response )
if err != nil {
t . Fatal ( err )
}
body := string ( encoded )
if ! strings . Contains ( body , ` "longitude":"120.123456" ` ) || ! strings . Contains ( body , ` "latitude":"30.456789" ` ) {
t . Fatalf ( "authorized response omitted precise coordinates: %s" , body )
}
if strings . Contains ( body , ` "address": ` ) || strings . Contains ( body , "敏感详细地址" ) {
t . Fatalf ( "precise location scope leaked address PII: %s" , body )
}
}
2026-07-27 13:12:43 +08:00
func TestPublicResponseProjectionRemovesCredentialAndAttachmentSecrets ( t * testing . T ) {
ctx , _ := updateContext ( http . MethodGet , "/staff/credential/credential-a" , "credential-a" , nil )
ctx . Set ( "Auth" , & types . JwtClaims { Extend : map [ string ] string { "location_scope" : "precise" } } )
response := protectPreciseLocation ( ctx , & models . StaffCredential { } , map [ string ] any {
"identity" : "credential-a" ,
"credential_type" : "installer" ,
"credential_no" : "CERT-123456" ,
"evidence_uri" : "private://evidence" ,
"file_uri" : "private://file" ,
"attachment_uri" : "private://attachment" ,
} )
encoded , err := json . Marshal ( response )
if err != nil {
t . Fatal ( err )
}
body := string ( encoded )
if ! strings . Contains ( body , ` "identity":"credential-a" ` ) || ! strings . Contains ( body , ` "credential_type":"installer" ` ) {
t . Fatalf ( "safe credential fields were removed: %s" , body )
}
for _ , forbidden := range [ ] string { "credential_no" , "evidence_uri" , "file_uri" , "attachment_uri" , "CERT-123456" , "private://" } {
if strings . Contains ( body , forbidden ) {
t . Fatalf ( "credential response leaked %s: %s" , forbidden , body )
}
}
}
func TestPlatformAccountDetailMasksDisplayNameAndAvatarWithPreciseScope ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platfrom_account" WHERE identity = $1 ORDER BY "platfrom_account"."id" LIMIT $2 ` ) ) .
WithArgs ( "account-a" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "username" , "display_name" , "avatar" , "password_hash" , "platform_role_code" , "phone" } ) .
AddRow ( uint64 ( 1 ) , "account-a" , nil , nil , "enabled" , 1 , "operator" , "张三" , "private://avatar" , "hash" , "finance_operator" , "13800138000" ) )
ctx , recorder := updateContext ( http . MethodGet , "/platform/platfrom_account/account-a" , "account-a" , nil )
ctx . Set ( "Auth" , & types . JwtClaims { Extend : map [ string ] string { "location_scope" : "precise" } } )
GetPlatfromAccount ( ctx )
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if ! strings . Contains ( body , ` "display_name_masked" ` ) || ! strings . Contains ( body , ` "phone_masked":"138****8000" ` ) {
t . Fatalf ( "platform account detail omitted masked PII: %s" , body )
}
for _ , forbidden := range [ ] string { ` "display_name": ` , ` "avatar": ` , "张三" , "private://avatar" } {
if strings . Contains ( body , forbidden ) {
t . Fatalf ( "platform account detail leaked %s: %s" , forbidden , body )
}
}
assertMockExpectations ( t , mock )
}
func TestPlatformAccountListMasksDisplayNameAndAvatar ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "platfrom_account" ` ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 1 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platfrom_account" ORDER BY created_at desc LIMIT $1 ` ) ) .
WithArgs ( 20 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "username" , "display_name" , "avatar" , "password_hash" , "platform_role_code" , "phone" } ) .
AddRow ( uint64 ( 1 ) , "account-a" , nil , nil , "enabled" , 1 , "operator" , "张三" , "private://avatar" , "hash" , "finance_operator" , "13800138000" ) )
ctx , recorder := updateContext ( http . MethodGet , "/platform/platfrom_account" , "" , nil )
ListPlatfromAccount ( ctx )
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if ! strings . Contains ( body , ` "display_name_masked" ` ) || strings . Contains ( body , ` "display_name": ` ) || strings . Contains ( body , ` "avatar": ` ) {
t . Fatalf ( "platform account list did not apply the masked projection: %s" , body )
}
assertMockExpectations ( t , mock )
}
func TestNonRootCannotManagePlatformRolesOrMenus ( t * testing . T ) {
tests := [ ] struct {
name string
handler gin . HandlerFunc
method string
target string
identity string
body string
} {
{ "create role" , CreatePlatformRole , http . MethodPost , "/platform/platform_role" , "" , ` { "role_code":"auditor","name":"Auditor"} ` } ,
{ "create menu" , CreatePlatformMenu , http . MethodPost , "/platform/platform_menu" , "" , ` { "menu_code":"audit","name":"Audit","path":"/audit"} ` } ,
{ "update menu status" , UpdatePlatformMenuStatus , http . MethodPatch , "/platform/platform_menu/menu-a/status" , "menu-a" , ` { "status":"disabled"} ` } ,
{ "archive menu" , ArchivePlatformMenu , http . MethodDelete , "/platform/platform_menu/menu-a" , "menu-a" , ` ` } ,
{ "replace role menus" , ReplacePlatformRoleMenus , http . MethodPut , "/platform/platform_role/role-a/menu" , "role-a" , ` { "menu_identities":[]} ` } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
ctx , recorder := updateContext ( test . method , test . target , test . identity , [ ] byte ( test . body ) )
ctx . Set ( "Auth" , & types . JwtClaims { Role : "platform_operator" } )
test . handler ( ctx )
assertResponseCode ( t , recorder , int32 ( status . Code ( errcode . ErrPermissionDenied ) ) )
assertMockExpectations ( t , mock )
} )
}
}
2026-07-27 12:48:35 +08:00
func TestPlatformMenuAllowsOnlyAssignedDomain ( t * testing . T ) {
menus := [ ] models . PlatformMenu {
{ MenuCode : "finance" , Path : "/finance" } ,
{ MenuCode : "fin_payment" , Path : "/finance/fin-payment" } ,
}
if ! platformMenuAllowsPath ( menus , "/heqi/platform/v1/finance/fin_payment" ) {
t . Fatal ( "assigned finance domain should be allowed" )
}
if platformMenuAllowsPath ( menus , "/heqi/platform/v1/user/account" ) {
t . Fatal ( "unassigned user domain should be denied" )
}
}
func TestCreatePlatformAccountRequiresAssignableNonRootRole ( t * testing . T ) {
tests := [ ] struct {
name string
body string
} {
{ "missing role" , ` { "username":"operator","password":"secure-password"} ` } ,
{ "root role" , ` { "username":"operator","password":"secure-password","platform_role_code":"root"} ` } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
ctx , recorder := updateContext ( http . MethodPost , "/platform/platfrom_account" , "" , [ ] byte ( test . body ) )
2026-07-27 13:12:43 +08:00
ctx . Set ( "Auth" , & types . JwtClaims { Role : "root" } )
2026-07-27 12:48:35 +08:00
CreatePlatfromAccount ( ctx )
assertResponseCode ( t , recorder , int32 ( status . Code ( errcode . ErrInvalidArgument ) ) )
assertMockExpectations ( t , mock )
} )
}
}
2026-07-27 13:12:43 +08:00
func TestNonRootCannotAssignPlatformAccountRole ( t * testing . T ) {
tests := [ ] struct {
name string
handler gin . HandlerFunc
method string
identity string
body string
} {
{ "create account" , CreatePlatfromAccount , http . MethodPost , "" , ` { "username":"operator","password":"secure-password","platform_role_code":"auditor"} ` } ,
{ "change account role" , UpdatePlatfromAccount , http . MethodPut , "account-a" , ` { "platform_role_code":"auditor"} ` } ,
}
for _ , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
ctx , recorder := updateContext ( test . method , "/platform/platfrom_account/" + test . identity , test . identity , [ ] byte ( test . body ) )
ctx . Set ( "Auth" , & types . JwtClaims { Role : "platform_operator" } )
test . handler ( ctx )
assertResponseCode ( t , recorder , int32 ( status . Code ( errcode . ErrPermissionDenied ) ) )
assertMockExpectations ( t , mock )
} )
}
}
2026-07-27 11:13:55 +08:00
func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
2026-07-27 14:28:23 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "safe_event_disposal" WHERE safe_event_identity = $1 ` ) ) .
2026-07-27 11:13:55 +08:00
WithArgs ( "event-a" ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 1 ) )
2026-07-27 14:28:23 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "safe_event_disposal" WHERE safe_event_identity = $1 ORDER BY created_at asc LIMIT $2 ` ) ) .
2026-07-27 11:13:55 +08:00
WithArgs ( "event-a" , 20 ) .
2026-07-27 14:28:23 +08:00
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "safe_event_identity" , "action" , "reason" , "operator_identity" } ) .
2026-07-27 11:13:55 +08:00
AddRow ( uint64 ( 9 ) , "disposal-a" , nil , nil , "enabled" , 1 , "event-a" , "close" , "resolved" , "operator-a" ) )
2026-07-27 14:28:23 +08:00
ctx , recorder := updateContext ( http . MethodGet , "/safety/safe_event/event-a/disposals" , "event-a" , nil )
2026-07-27 11:13:55 +08:00
ListSafetyEventDisposals ( ctx )
assertResponseCode ( t , recorder , 0 )
2026-07-27 14:28:23 +08:00
if ! strings . Contains ( recorder . Body . String ( ) , ` "safe_event_identity":"event-a" ` ) || strings . Contains ( recorder . Body . String ( ) , ` "id": ` ) {
2026-07-27 11:13:55 +08:00
t . Fatalf ( "disposal history did not keep the event identity-only shape: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 10:25:04 +08:00
func TestListGasAccountProjectsGasBasicIdentityAndNeverReturnsRelationID ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "gas_account" ` ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 1 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1 ` ) ) .
WithArgs ( 20 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "gas_basic_id" , "username" , "display_name" , "password_hash" , "role_code" } ) .
AddRow ( uint64 ( 9 ) , "account-a" , nil , nil , "enabled" , 1 , uint64 ( 7 ) , "operator" , "Operator" , "hash" , "admin" ) )
2026-07-27 10:42:16 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 7 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 7 ) , "gas-a" ) )
2026-07-27 10:25:04 +08:00
ctx , recorder := updateContext ( http . MethodGet , "/gas/gas_account" , "" , nil )
ListGasAccount ( ctx )
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if ! strings . Contains ( body , ` "gas_basic_identity":"gas-a" ` ) || strings . Contains ( body , ` "gas_basic_id" ` ) || strings . Contains ( body , ` "id": ` ) {
t . Fatalf ( "account list did not return the public relation shape: %s" , body )
}
assertMockExpectations ( t , mock )
}
2026-07-27 10:42:16 +08:00
func TestListGasAccountPreloadsRelationIdentitiesInOneQuery ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "gas_account" ` ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 2 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1 ` ) ) .
WithArgs ( 20 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "gas_basic_id" , "username" , "display_name" , "password_hash" , "role_code" } ) .
AddRow ( uint64 ( 9 ) , "account-a" , nil , nil , "enabled" , 1 , uint64 ( 7 ) , "operator-a" , "Operator A" , "hash" , "admin" ) .
AddRow ( uint64 ( 10 ) , "account-b" , nil , nil , "enabled" , 1 , uint64 ( 8 ) , "operator-b" , "Operator B" , "hash" , "admin" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1,$2) ` ) ) .
WithArgs ( uint64 ( 7 ) , uint64 ( 8 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 7 ) , "gas-a" ) . AddRow ( uint64 ( 8 ) , "gas-b" ) )
ctx , recorder := updateContext ( http . MethodGet , "/gas/gas_account" , "" , nil )
ListGasAccount ( ctx )
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if ! strings . Contains ( body , ` "gas_basic_identity":"gas-a" ` ) || ! strings . Contains ( body , ` "gas_basic_identity":"gas-b" ` ) {
t . Fatalf ( "account list omitted preloaded relation identities: %s" , body )
}
assertMockExpectations ( t , mock )
}
func TestListGasAccountFailsWhenRelationIdentityProjectionCannotLoad ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "gas_account" ` ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 1 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "gas_account" ORDER BY created_at desc LIMIT $1 ` ) ) .
WithArgs ( 20 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "gas_basic_id" , "username" , "display_name" , "password_hash" , "role_code" } ) .
AddRow ( uint64 ( 9 ) , "account-a" , nil , nil , "enabled" , 1 , uint64 ( 7 ) , "operator" , "Operator" , "hash" , "admin" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "gas_basic" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 7 ) ) .
WillReturnError ( errors . New ( "relation lookup unavailable" ) )
ctx , recorder := updateContext ( http . MethodGet , "/gas/gas_account" , "" , nil )
ListGasAccount ( ctx )
if strings . Contains ( recorder . Body . String ( ) , ` "code":0 ` ) {
t . Fatalf ( "relation projection failure was returned as success: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 02:47:39 +08:00
func TestGetDeliveryTrackOrdersAndMasksPointsWithoutPreciseLocationScope ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
now := time . Now ( ) . UTC ( )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "delivery_track" WHERE identity = $1 ORDER BY "delivery_track"."id" LIMIT $2 ` ) ) .
WithArgs ( "track-a" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "delivery_task_id" , "started_at" , "completed_at" } ) .
AddRow ( uint64 ( 7 ) , "track-a" , nil , nil , "enabled" , 1 , uint64 ( 8 ) , now , nil ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "delivery_track_point" WHERE delivery_track_id = $1 ORDER BY occurred_at asc ` ) ) .
WithArgs ( uint64 ( 7 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "delivery_track_id" , "point_type" , "occurred_at" , "longitude" , "latitude" } ) .
AddRow ( uint64 ( 9 ) , "point-a" , nil , nil , "enabled" , 1 , uint64 ( 7 ) , "arrival" , now , "120.123" , "30.456" ) )
2026-07-27 10:42:16 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "delivery_task" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 8 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 8 ) , "task-a" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 7 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 7 ) , "track-a" ) )
2026-07-27 02:47:39 +08:00
ctx , recorder := updateContext ( http . MethodGet , "/delivery/delivery_track/track-a" , "track-a" , nil )
GetDeliveryTrack ( ctx )
assertResponseCode ( t , recorder , 0 )
if strings . Contains ( recorder . Body . String ( ) , "120.123" ) || strings . Contains ( recorder . Body . String ( ) , "30.456" ) {
t . Fatalf ( "unauthorized response exposed precise coordinates: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 12:02:08 +08:00
func TestListDeliveryTrackPointsMasksCoordinatesWithoutPreciseLocationScope ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
now := time . Now ( ) . UTC ( )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT count(*) FROM "delivery_track_point" ` ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "count" } ) . AddRow ( 1 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "delivery_track_point" ORDER BY created_at desc LIMIT $1 ` ) ) .
WithArgs ( 20 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "delivery_track_id" , "point_type" , "occurred_at" , "longitude" , "latitude" } ) .
AddRow ( uint64 ( 9 ) , "point-a" , now , now , "enabled" , 1 , uint64 ( 7 ) , "arrival" , now , "120.123456" , "30.456789" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 7 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 7 ) , "track-a" ) )
ctx , recorder := updateContext ( http . MethodGet , "/delivery/delivery_track_point" , "" , nil )
listResource ( ctx , & models . DeliveryTrackPoint { } )
assertResponseCode ( t , recorder , 0 )
body := recorder . Body . String ( )
if strings . Contains ( body , "120.123456" ) || strings . Contains ( body , "30.456789" ) {
t . Fatalf ( "track-point list exposed precise coordinates without scope: %s" , body )
}
if ! strings . Contains ( body , ` "delivery_track_identity":"track-a" ` ) {
t . Fatalf ( "track-point list omitted its public relation identity: %s" , body )
}
assertMockExpectations ( t , mock )
}
func TestGetDeliveryTrackPointReturnsCoordinatesWithPreciseLocationScope ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
now := time . Now ( ) . UTC ( )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "delivery_track_point" WHERE identity = $1 ORDER BY "delivery_track_point"."id" LIMIT $2 ` ) ) .
WithArgs ( "point-a" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "delivery_track_id" , "point_type" , "occurred_at" , "longitude" , "latitude" } ) .
AddRow ( uint64 ( 9 ) , "point-a" , now , now , "enabled" , 1 , uint64 ( 7 ) , "arrival" , now , "120.123456" , "30.456789" ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT "id","identity" FROM "delivery_track" WHERE id IN ($1) ` ) ) .
WithArgs ( uint64 ( 7 ) ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" } ) . AddRow ( uint64 ( 7 ) , "track-a" ) )
ctx , recorder := updateContext ( http . MethodGet , "/delivery/delivery_track_point/point-a" , "point-a" , nil )
ctx . Set ( "Auth" , & types . JwtClaims { Extend : map [ string ] string { "location_scope" : "precise" } } )
getResource ( ctx , & models . DeliveryTrackPoint { } )
assertResponseCode ( t , recorder , 0 )
if ! strings . Contains ( recorder . Body . String ( ) , "120.123456" ) || ! strings . Contains ( recorder . Body . String ( ) , "30.456789" ) {
t . Fatalf ( "authorized track-point detail omitted precise coordinates: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 02:47:39 +08:00
func TestDisposeSafetyEventUpdatesEventAndAppendsOperatorActionTransactionally ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
now := time . Now ( ) . UTC ( )
mock . ExpectBegin ( )
2026-07-27 14:28:23 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "safe_event" WHERE identity = $1 ORDER BY "safe_event"."id" LIMIT $2 ` ) ) .
2026-07-27 02:47:39 +08:00
WithArgs ( "event-a" , 1 ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "event_code" , "level" , "title" , "smart_cylinder_valve_identity" , "sla_at" } ) .
AddRow ( uint64 ( 3 ) , "event-a" , now , now , "open" , 1 , "E-1" , 2 , "alarm" , "valve-a" , nil ) )
2026-07-27 14:28:23 +08:00
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "safe_event" SET "status"=$1,"updated_at"=$2 WHERE identity = $3 ` ) ) .
2026-07-27 02:47:39 +08:00
WithArgs ( "disposed" , sqlmock . AnyArg ( ) , "event-a" ) .
WillReturnResult ( sqlmock . NewResult ( 0 , 1 ) )
2026-07-27 14:28:23 +08:00
mock . ExpectQuery ( regexp . QuoteMeta ( ` INSERT INTO "safe_event_disposal" ("identity","created_at","updated_at","status","version","safe_event_identity","action","reason","operator_identity") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) RETURNING "id" ` ) ) .
2026-07-27 02:47:39 +08:00
WithArgs ( sqlmock . AnyArg ( ) , sqlmock . AnyArg ( ) , sqlmock . AnyArg ( ) , "enabled" , 1 , "event-a" , "close" , "resolved" , "operator-a" ) .
WillReturnRows ( sqlmock . NewRows ( [ ] string { "id" } ) . AddRow ( uint64 ( 1 ) ) )
mock . ExpectCommit ( )
2026-07-27 14:28:23 +08:00
ctx , recorder := updateContext ( http . MethodPost , "/safety/safe_event/event-a/disposals" , "event-a" , [ ] byte ( ` { "action":"close","reason":"resolved"} ` ) )
2026-07-27 02:47:39 +08:00
ctx . Set ( "Auth" , & types . JwtClaims { Identity : "operator-a" } )
DisposeSafetyEvent ( ctx )
assertResponseCode ( t , recorder , 0 )
if ! strings . Contains ( recorder . Body . String ( ) , ` "operator_identity":"operator-a" ` ) {
t . Fatalf ( "disposal omitted its operator: %s" , recorder . Body . String ( ) )
}
assertMockExpectations ( t , mock )
}
2026-07-27 02:03:26 +08:00
func TestReplacePlatformRoleMenusAllowsAnEmptySetToClearAssignmentsTransactionally ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectBegin ( )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2 ` ) ) .
WithArgs ( "role-a" , 1 ) .
WillReturnRows ( platformRoleRows ( "role-a" ) )
mock . ExpectExec ( regexp . QuoteMeta ( ` DELETE FROM "platform_role_menu_relation" WHERE platform_role_id = $1 ` ) ) .
WithArgs ( uint64 ( 1 ) ) .
WillReturnResult ( sqlmock . NewResult ( 0 , 2 ) )
mock . ExpectCommit ( )
ctx , recorder := updateContext ( http . MethodPut , "/roles/role-a/menus" , "role-a" , [ ] byte ( ` { "menu_identities":[]} ` ) )
2026-07-27 13:12:43 +08:00
ctx . Set ( "Auth" , & types . JwtClaims { Role : "root" } )
2026-07-27 02:03:26 +08:00
ReplacePlatformRoleMenus ( ctx )
assertResponseCode ( t , recorder , 0 )
assertMockExpectations ( t , mock )
}
2026-07-27 02:11:42 +08:00
func TestReplacePlatformRoleMenusRejectsSystemRoleBeforeChangingRelations ( t * testing . T ) {
_ , mock := setupPlatformRoleDatabase ( t )
mock . ExpectBegin ( )
mock . ExpectQuery ( regexp . QuoteMeta ( ` SELECT * FROM "platform_role" WHERE identity = $1 ORDER BY "platform_role"."id" LIMIT $2 ` ) ) .
WithArgs ( "root-role" , 1 ) .
WillReturnRows ( platformSystemRoleRows ( "root-role" ) )
mock . ExpectRollback ( )
ctx , recorder := updateContext ( http . MethodPut , "/roles/root-role/menus" , "root-role" , [ ] byte ( ` { "menu_identities":[]} ` ) )
2026-07-27 13:12:43 +08:00
ctx . Set ( "Auth" , & types . JwtClaims { Role : "root" } )
2026-07-27 02:11:42 +08:00
ReplacePlatformRoleMenus ( ctx )
assertResponseCode ( t , recorder , int32 ( status . Code ( errcode . ErrInvalidArgument ) ) )
assertMockExpectations ( t , mock )
}
2026-07-27 01:49:02 +08:00
type responseBody struct {
Code int32 ` json:"code" `
Message string ` json:"message" `
Details json . RawMessage ` json:"details" `
}
func setupPlatformRoleDatabase ( t * testing . T ) ( * gorm . DB , sqlmock . Sqlmock ) {
t . Helper ( )
previous := impl . DBService
sqlDatabase , mock , err := sqlmock . New ( )
if err != nil {
t . Fatal ( err )
}
database , err := gorm . Open ( postgres . New ( postgres . Config { Conn : sqlDatabase } ) , & gorm . Config { } )
if err != nil {
t . Fatal ( err )
}
impl . DBService = database
t . Cleanup ( func ( ) {
impl . DBService = previous
_ = sqlDatabase . Close ( )
} )
return database , mock
}
func platformRoleRows ( identity string ) * sqlmock . Rows {
return sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "role_code" , "name" , "data_scope" , "is_system" } ) .
AddRow ( uint64 ( 1 ) , identity , nil , nil , "enabled" , 1 , identity , identity , "global" , false )
}
2026-07-27 02:11:42 +08:00
func platformSystemRoleRows ( identity string ) * sqlmock . Rows {
return sqlmock . NewRows ( [ ] string { "id" , "identity" , "created_at" , "updated_at" , "status" , "version" , "role_code" , "name" , "data_scope" , "is_system" } ) .
AddRow ( uint64 ( 1 ) , identity , nil , nil , "enabled" , 1 , identity , identity , "global" , true )
}
2026-07-27 01:49:02 +08:00
func updateContext ( method , target , identity string , body [ ] byte ) ( * gin . Context , * httptest . ResponseRecorder ) {
recorder := httptest . NewRecorder ( )
ctx , _ := gin . CreateTestContext ( recorder )
ctx . Request = httptest . NewRequest ( method , target , bytes . NewReader ( body ) )
ctx . Request . Header . Set ( "Content-Type" , "application/json" )
ctx . Params = gin . Params { { Key : "identity" , Value : identity } }
return ctx , recorder
}
func assertResponseCode ( t * testing . T , recorder * httptest . ResponseRecorder , want int32 ) {
t . Helper ( )
var reply responseBody
if err := json . Unmarshal ( recorder . Body . Bytes ( ) , & reply ) ; err != nil {
t . Fatal ( err )
}
if reply . Code != want {
t . Fatalf ( "response code = %d, want %d: %s" , reply . Code , want , recorder . Body . String ( ) )
}
}
func assertMockExpectations ( t * testing . T , mock sqlmock . Sqlmock ) {
t . Helper ( )
if err := mock . ExpectationsWereMet ( ) ; err != nil {
t . Fatal ( err )
}
}
2026-07-27 01:11:38 +08:00
func assertContract ( t * testing . T , contracts [ ] ResourceContract , domain , name string , mode ResourceMode , pageKind string ) {
t . Helper ( )
for _ , contract := range contracts {
if contract . Domain == domain && contract . Name == name && contract . Mode == mode && contract . PageKind == pageKind {
return
}
}
t . Fatalf ( "missing resource contract %s/%s with mode %q and page kind %q" , domain , name , mode , pageKind )
}