fix(platform): guard approval transitions
This commit is contained in:
@@ -14,6 +14,8 @@ import (
|
||||
"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,
|
||||
@@ -40,6 +42,10 @@ func ApproveAudit(ctx *gin.Context) {
|
||||
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.AudApproval
|
||||
@@ -47,6 +53,9 @@ func ApproveAudit(ctx *gin.Context) {
|
||||
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,
|
||||
@@ -54,10 +63,10 @@ func ApproveAudit(ctx *gin.Context) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result := transaction.Model(&models.AudApproval{}).Where("identity = ?", approval.Identity).Updates(values); result.Error != nil {
|
||||
if result := transaction.Model(&models.AudApproval{}).Where("identity = ? AND status = ?", approval.Identity, "pending").Updates(values); result.Error != nil {
|
||||
return result.Error
|
||||
} else if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
return errApprovalNotProcessable
|
||||
}
|
||||
after, err := json.Marshal(values)
|
||||
if err != nil {
|
||||
@@ -74,6 +83,10 @@ func ApproveAudit(ctx *gin.Context) {
|
||||
}).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
|
||||
|
||||
@@ -8,8 +8,10 @@ import (
|
||||
"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) {
|
||||
@@ -34,8 +36,8 @@ func TestApproveAuditOnlyUpdatesApprovalFieldsAndAppendsOperationAudit(t *testin
|
||||
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 "aud_approval" SET "handled_at"=$1,"handler_identity"=$2,"opinion"=$3,"status"=$4,"updated_at"=$5 WHERE identity = $6`)).
|
||||
WithArgs(sqlmock.AnyArg(), "operator-a", "accepted", "approved", sqlmock.AnyArg(), "approval-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", "accepted", "approved", sqlmock.AnyArg(), "approval-a", "pending").
|
||||
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"`)).
|
||||
@@ -53,6 +55,78 @@ func TestApproveAuditOnlyUpdatesApprovalFieldsAndAppendsOperationAudit(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type jsonContaining string
|
||||
|
||||
func (expected jsonContaining) Match(value driver.Value) bool {
|
||||
|
||||
Reference in New Issue
Block a user