2026-09-30 11:20:29 +07:00
package processor
import (
2026-09-30 12:33:20 +07:00
"context"
2026-09-30 11:20:29 +07:00
"testing"
"time"
2026-09-30 12:33:20 +07:00
"github.com/google/uuid"
2026-09-30 11:20:29 +07:00
"github.com/stretchr/testify/assert"
2026-09-30 12:33:20 +07:00
"github.com/stretchr/testify/require"
"golang.org/x/crypto/bcrypt"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
2026-09-30 11:20:29 +07:00
)
func TestCheckNewPin ( t * testing . T ) {
birth := time . Date ( 1990 , 3 , 14 , 0 , 0 , 0 , 0 , time . UTC )
for _ , ok := range [] string { "482913" , "019283" , "135790" , "112233" } {
assert . NoError ( t , checkNewPin ( ok , ok , & birth ), ok )
}
for name , c := range map [ string ][ 2 ] string {
2026-09-30 12:33:20 +07:00
"too short" : { "12345" , "12345" },
"too long" : { "1234567" , "1234567" },
"not digits" : { "12a456" , "12a456" },
"confirmation" : { "482913" , "482914" },
"one digit" : { "111111" , "111111" },
"zeros" : { "000000" , "000000" },
"run up" : { "123456" , "123456" },
"run up from 4" : { "456789" , "456789" },
"run down" : { "654321" , "654321" },
"run down from 9" : { "987654" , "987654" },
"birth date DDMMYY" : { "140390" , "140390" },
"birth date YYMMDD" : { "900314" , "900314" },
2026-09-30 11:20:29 +07:00
} {
err := checkNewPin ( c [ 0 ], c [ 1 ], & birth )
assert . ErrorIs ( t , err , ErrInvalidPinInput , name )
assert . NotContains ( t , err . Error (), c [ 0 ], "%s: the message must not echo the PIN" , name )
}
// Without a birth date only the other rules apply.
assert . NoError ( t , checkNewPin ( "140390" , "140390" , nil ))
}
2026-09-30 12:33:20 +07:00
// pinRepoFake holds one customer's PIN state, with the lock rules of RecordFailure.
type pinRepoFake struct {
repository . CustomerPinRepository
state repository . CustomerPinState
}
func ( f * pinRepoFake ) GetState ( context . Context , uuid . UUID ) ( * repository . CustomerPinState , error ) {
s := f . state
return & s , nil
}
func ( f * pinRepoFake ) RecordFailure ( _ context . Context , _ uuid . UUID , maxAttempts int , now , lockUntil time . Time ) ( int , * time . Time , error ) {
if f . state . LockedUntil != nil && ! f . state . LockedUntil . After ( now ) {
f . state . FailedAttempts , f . state . LockedUntil = 1 , nil
} else {
f . state . FailedAttempts ++
if f . state . FailedAttempts >= maxAttempts {
f . state . LockedUntil = & lockUntil
}
}
return f . state . FailedAttempts , f . state . LockedUntil , nil
}
func ( f * pinRepoFake ) ClearFailures ( context . Context , uuid . UUID ) error {
f . state . FailedAttempts , f . state . LockedUntil = 0 , nil
return nil
}
func ( f * pinRepoFake ) InsertEvent ( context . Context , repository . CustomerSecurityEvent ) error {
return nil
}
func TestCustomerPin_LockIsPushedThroughFCM ( t * testing . T ) {
customer := uuid . New ()
hash , err := bcrypt . GenerateFromPassword ([] byte ( "482913" ), bcrypt . MinCost )
require . NoError ( t , err )
h := string ( hash )
repo := & pinRepoFake { state : repository . CustomerPinState { CustomerID : customer , PinHash : & h }}
notifier := & notifierFake {}
p := NewCustomerPinProcessor ( repo , nil , notifier )
now := time . Date ( 2026 , 9 , 30 , 3 , 0 , 0 , 0 , time . UTC )
p . now = func () time . Time { return now }
ctx := context . Background ()
for i := 0 ; i < 4 ; i ++ {
_ = p . VerifyPin ( ctx , customer , "000000" , PinActionPay , models . CustomerPinRequestInfo {})
}
assert . Empty ( t , notifier . pushes [ customer ], "no push before the PIN locks" )
err = p . VerifyPin ( ctx , customer , "000000" , PinActionPay , models . CustomerPinRequestInfo {})
var pinErr * PinError
require . ErrorAs ( t , err , & pinErr )
assert . Equal ( t , PinErrLocked , pinErr . Code )
// Attempts while locked do not push again.
_ = p . VerifyPin ( ctx , customer , "482913" , PinActionPay , models . CustomerPinRequestInfo {})
require . Len ( t , notifier . pushes [ customer ], 1 )
push := notifier . pushes [ customer ][ 0 ]
assert . Equal ( t , "PIN terkunci" , push . title )
assert . Equal ( t , "PIN EnakPoint kamu terkunci sampai 30 Sep 2026 10:30 WIB karena salah dimasukkan 5 kali. Jika ini bukan kamu, segera reset PIN lewat aplikasi." , push . body )
assert . Equal ( t , map [ string ] string { "type" : NotificationTypePinLocked , "locked_until" : "2026-09-30T03:30:00Z" }, push . data )
}