2026-07-27 03:18:41 +08:00
package platform
import (
"database/sql/driver"
"net/http"
"regexp"
"strings"
"testing"
"time"
2026-07-27 09:14:36 +08:00
"git.apinb.com/bsm-sdk/core/errcode"
2026-07-27 03:18:41 +08:00
"git.apinb.com/bsm-sdk/core/types"
"github.com/DATA-DOG/go-sqlmock"
2026-07-27 09:14:36 +08:00
"google.golang.org/grpc/status"
2026-07-27 03:18:41 +08:00
)
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 "aud_approval" WHERE identity = $1 ORDER BY "aud_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 ) )
2026-07-27 09:14:36 +08:00
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "aud_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" ) .
2026-07-27 03:18:41 +08:00
WillReturnResult ( sqlmock . NewResult ( 0 , 1 ) )
mock . ExpectQuery ( regexp . QuoteMeta ( ` INSERT INTO "aud_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" , "aud_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/aud_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 ( ) )
}
}
2026-07-27 09:14:36 +08:00
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/aud_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 "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2 ` ) ) .
WithArgs ( "approval-a" , 1 ) .
WillReturnRows ( approvalRows ( "approval-a" , "approved" , "applicant-a" ) )
mock . ExpectRollback ( )
ctx , recorder := updateContext ( http . MethodPost , "/audit/aud_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 "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2 ` ) ) .
WithArgs ( "approval-a" , 1 ) .
WillReturnRows ( approvalRows ( "approval-a" , "pending" , "operator-a" ) )
mock . ExpectRollback ( )
ctx , recorder := updateContext ( http . MethodPost , "/audit/aud_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 "aud_approval" WHERE identity = $1 ORDER BY "aud_approval"."id" LIMIT $2 ` ) ) .
WithArgs ( "approval-a" , 1 ) .
WillReturnRows ( approvalRows ( "approval-a" , "pending" , "applicant-a" ) )
mock . ExpectExec ( regexp . QuoteMeta ( ` UPDATE "aud_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/aud_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 )
}
2026-07-27 03:18:41 +08:00
type jsonContaining string
func ( expected jsonContaining ) Match ( value driver . Value ) bool {
actual , ok := value . ( string )
return ok && strings . Contains ( actual , string ( expected ) )
}