2026-08-04 22:33:16 +08:00
package taskclaim
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
"testing"
"time"
"cmbuyer/admin/internal/migrations"
"cmbuyer/admin/internal/storage/sqlite"
)
const (
testDeviceA = "10000000-0000-4000-8000-000000000001"
testDeviceB = "10000000-0000-4000-8000-000000000002"
testSessionA = "20000000-0000-4000-8000-000000000001"
testSessionB = "20000000-0000-4000-8000-000000000002"
testTaskA = "30000000-0000-4000-8000-000000000001"
testTaskB = "30000000-0000-4000-8000-000000000002"
testAuthA = "40000000-0000-4000-8000-000000000001"
testAuthB = "40000000-0000-4000-8000-000000000002"
testClaimRequestA = "50000000-0000-4000-8000-000000000001"
testClaimRequestB = "50000000-0000-4000-8000-000000000002"
testClaimRequestC = "50000000-0000-4000-8000-000000000003"
testRenewRequestA = "60000000-0000-4000-8000-000000000001"
testRenewRequestB = "60000000-0000-4000-8000-000000000002"
)
var testNow = time . Date ( 2026 , 8 , 4 , 1 , 2 , 3 , 123000000 , time . UTC )
func TestClaimReplayEmptyManualAndSecretRecovery ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertDevice ( t , database , testDeviceB , [] byte ( "device-b" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow . Add ( - time . Minute ), testNow . Add ( 10 * time . Minute ), true )
secret := bytes . Repeat ([] byte { 0x11 }, 32 )
store := mustStore ( t , database , secret , 30 * time . Second )
store . now = func () time . Time { return testNow }
command := ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA }
claimed , found , err := store . ClaimNext ( context . Background (), testDeviceA , command )
if err != nil || ! found {
t . Fatalf ( "ClaimNext = found %v, err %v" , found , err )
}
if claimed . Task . ID != testTaskA || claimed . Task . Version != 3 || claimed . Authorization . ID != testAuthA ||
claimed . Authorization . TaskVersion != 2 || claimed . Attempt . ClaimGeneration != 1 ||
len ( claimed . Attempt . ClaimToken ) != 64 || strings . ToLower ( claimed . Attempt . ClaimToken ) != claimed . Attempt . ClaimToken {
t . Fatalf ( "unexpected claim response: %#v" , claimed )
}
assertClaimState ( t , database , 1 , "CLAIMED" , "CLAIMED" )
assertNoPlaintextTokenColumnOrValue ( t , database , claimed . Attempt . ClaimToken )
replayed , found , err := store . ClaimNext ( context . Background (), testDeviceA , command )
if err != nil || ! found || ! reflect . DeepEqual ( replayed , claimed ) {
t . Fatalf ( "same request replay = %#v, found %v, err %v" , replayed , found , err )
}
restarted := mustStore ( t , database , secret , 30 * time . Second )
restarted . now = func () time . Time { return testNow . Add ( 5 * time . Second ) }
replayed , found , err = restarted . ClaimNext ( context . Background (), testDeviceA , command )
if err != nil || ! found || ! reflect . DeepEqual ( replayed , claimed ) {
t . Fatalf ( "restart replay = %#v, found %v, err %v" , replayed , found , err )
}
if _ , err := NewStore ( database , bytes . Repeat ([] byte { 0x22 }, 32 ), 30 * time . Second ); err == nil {
t . Fatal ( "NewStore accepted a secret that cannot rebuild existing claims" )
}
sameSession , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand {
SessionID : testSessionA , ClaimRequestID : "50000000-0000-4000-8000-000000000005" ,
})
if err != nil || ! found || sameSession . Attempt . ID != claimed . Attempt . ID || sameSession . Attempt . ClaimToken != claimed . Attempt . ClaimToken {
t . Fatalf ( "same-session recovery = %#v, found %v, err %v" , sameSession , found , err )
}
if _ , err := database . Exec ( `UPDATE order_authorizations SET goods_id='937122477376', sku_color='白色',
sku_size='L', quantity=3, total_price_cap='40.00', expires_at=? WHERE id=?` ,
formatTime ( testNow . Add ( 20 * time . Minute )), testAuthA ); err != nil {
t . Fatalf ( "mutate authorization source: %v" , err )
}
if _ , err := database . Exec ( `UPDATE tasks SET title='漂移标题', goods_id='937122477376', sku_color='白色',
sku_size='L', quantity=3, max_total_price='40.00' WHERE id=?` , testTaskA ); err != nil {
t . Fatalf ( "mutate task source: %v" , err )
}
afterDrift := mustStore ( t , database , secret , 30 * time . Second )
afterDrift . now = func () time . Time { return testNow . Add ( 6 * time . Second ) }
stable , found , err := afterDrift . ClaimNext ( context . Background (), testDeviceA , command )
if err != nil || ! found || ! reflect . DeepEqual ( stable , claimed ) {
t . Fatalf ( "source-drift replay = %#v, found %v, err %v; want original %#v" , stable , found , err , claimed )
}
if _ , _ , err := afterDrift . ClaimNext ( context . Background (), testDeviceA , ClaimCommand {
SessionID : testSessionA , ClaimRequestID : "50000000-0000-4000-8000-000000000006" ,
}); ! errors . Is ( err , ErrRequiresManual ) {
t . Fatalf ( "new recovery after source drift error = %v" , err )
}
manualCommand := ClaimCommand { SessionID : testSessionB , ClaimRequestID : testClaimRequestB }
if _ , _ , err := store . ClaimNext ( context . Background (), testDeviceA , manualCommand ); ! errors . Is ( err , ErrRequiresManual ) {
t . Fatalf ( "different session error = %v, want ErrRequiresManual" , err )
}
if _ , _ , err := store . ClaimNext ( context . Background (), testDeviceA , manualCommand ); ! errors . Is ( err , ErrRequiresManual ) {
t . Fatalf ( "manual replay error = %v, want ErrRequiresManual" , err )
}
assertClaimState ( t , database , 1 , "CLAIMED" , "CLAIMED" )
emptyCommand := ClaimCommand { SessionID : testSessionB , ClaimRequestID : testClaimRequestC }
if _ , found , err := store . ClaimNext ( context . Background (), testDeviceB , emptyCommand ); err != nil || found {
t . Fatalf ( "empty claim = found %v, err %v" , found , err )
}
insertCandidate ( t , database , testTaskB , testAuthB , testNow , testNow . Add ( 10 * time . Minute ), true )
if _ , found , err := store . ClaimNext ( context . Background (), testDeviceB , emptyCommand ); err != nil || found {
t . Fatalf ( "persisted EMPTY replay = found %v, err %v" , found , err )
}
claimedB , found , err := store . ClaimNext ( context . Background (), testDeviceB , ClaimCommand {
SessionID : testSessionB , ClaimRequestID : "50000000-0000-4000-8000-000000000004" ,
})
if err != nil || ! found || claimedB . Task . ID != testTaskB {
t . Fatalf ( "new request after EMPTY = %#v, found %v, err %v" , claimedB , found , err )
}
var distinctNonces int
if err := database . QueryRow ( "SELECT COUNT(DISTINCT claim_nonce) FROM purchase_attempt_claims" ). Scan ( & distinctNonces ); err != nil || distinctNonces != 2 {
t . Fatalf ( "distinct claim nonces = %d, err %v" , distinctNonces , err )
}
}
func TestSameSessionOrderingRecoveryNeverClaimsAnotherTask ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( 10 * time . Minute ), true )
insertCandidate ( t , database , testTaskB , testAuthB , testNow . Add ( time . Second ), testNow . Add ( 10 * time . Minute ), true )
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x21 }, 32 ), time . Minute )
store . now = func () time . Time { return testNow }
claimed , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err != nil || ! found || claimed . Task . ID != testTaskA {
t . Fatalf ( "initial claim = %#v, found %v, err %v" , claimed , found , err )
}
if _ , err := database . Exec ( "UPDATE tasks SET status='ORDERING', version=version+1 WHERE id=?" , testTaskA ); err != nil {
t . Fatal ( err )
}
if _ , err := database . Exec ( "UPDATE purchase_attempts SET status='ORDERING' WHERE id=?" , claimed . Attempt . ID ); err != nil {
t . Fatal ( err )
}
recovered , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestB })
if err != nil || ! found || recovered . Attempt . ID != claimed . Attempt . ID || recovered . Task . ID != testTaskA {
t . Fatalf ( "ORDERING recovery = %#v, found %v, err %v" , recovered , found , err )
}
var attempts int
var taskBStatus string
if err := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempts" ). Scan ( & attempts ); err != nil {
t . Fatal ( err )
}
if err := database . QueryRow ( "SELECT status FROM tasks WHERE id=?" , testTaskB ). Scan ( & taskBStatus ); err != nil {
t . Fatal ( err )
}
if attempts != 1 || taskBStatus != "PENDING" {
t . Fatalf ( "ORDERING recovery attempts/taskB = %d/%s" , attempts , taskBStatus )
}
if _ , err := database . Exec ( "UPDATE tasks SET version=version+1 WHERE id=?" , testTaskA ); err != nil {
t . Fatal ( err )
}
if _ , _ , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestC }); ! errors . Is ( err , ErrRequiresManual ) {
t . Fatalf ( "ORDERING recovery with drifted version error = %v" , err )
}
if err := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempts" ). Scan ( & attempts ); err != nil || attempts != 1 {
t . Fatalf ( "attempts after drifted ORDERING recovery = %d, err %v" , attempts , err )
}
}
func TestClaimRollsBackEveryBusinessMutationOnLateFailure ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( time . Minute ), true )
if _ , err := database . Exec ( `CREATE TRIGGER fail_claim_insert BEFORE INSERT ON purchase_attempt_claims
BEGIN SELECT RAISE(ABORT, 'injected claim failure'); END` ); err != nil {
t . Fatal ( err )
}
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x31 }, 32 ), 30 * time . Second )
store . now = func () time . Time { return testNow }
if _ , _ , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA }); err == nil {
t . Fatal ( "ClaimNext succeeded despite injected late failure" )
}
assertClaimState ( t , database , 0 , "PENDING" , "ACTIVE" )
for _ , table := range [] string { "purchase_attempts" , "task_claim_requests" } {
var count int
if err := database . QueryRow ( "SELECT COUNT(*) FROM " + table ). Scan ( & count ); err != nil || count != 0 {
t . Fatalf ( "%s rows after rollback = %d, err %v" , table , count , err )
}
}
}
2026-08-05 01:25:23 +08:00
func TestClaimRejectsOutOfBoundsCandidatesWithoutBusinessMutation ( t * testing . T ) {
mutations := map [ string ] func ( * testing . T , * sql . DB ){
2026-08-05 01:39:35 +08:00
"task invalid utf8 title" : func ( t * testing . T , database * sql . DB ) {
2026-08-05 01:25:23 +08:00
execClaimSQL ( t , database , `UPDATE tasks SET title=? WHERE id=?` , string ([] byte { 0xff }), testTaskA )
},
2026-08-05 01:39:35 +08:00
"task overlong title" : func ( t * testing . T , database * sql . DB ) {
2026-08-05 01:25:23 +08:00
execClaimSQL ( t , database , `UPDATE tasks SET title=? WHERE id=?` , strings . Repeat ( "😀" , 121 ), testTaskA )
},
2026-08-05 01:39:35 +08:00
"task overlong goods id" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE tasks SET goods_id=? WHERE id=?` , strings . Repeat ( "1" , 33 ), testTaskA )
2026-08-05 01:25:23 +08:00
},
2026-08-05 01:39:35 +08:00
"task invalid utf8 color" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE tasks SET sku_color=? WHERE id=?` , string ([] byte { 0xff }), testTaskA )
2026-08-05 01:25:23 +08:00
},
2026-08-05 01:39:35 +08:00
"task overlong color" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE tasks SET sku_color=? WHERE id=?` , strings . Repeat ( "色" , 81 ), testTaskA )
2026-08-05 01:25:23 +08:00
},
2026-08-05 01:39:35 +08:00
"task overlong size" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE tasks SET sku_size=? WHERE id=?` , strings . Repeat ( "码" , 81 ), testTaskA )
},
"task overlong money" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE tasks SET max_total_price=? WHERE id=?` , strings . Repeat ( "1" , 30 ) + ".00" , testTaskA )
},
"authorization overlong goods id" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE order_authorizations SET goods_id=? WHERE id=?` , strings . Repeat ( "1" , 33 ), testAuthA )
},
"authorization invalid utf8 color" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE order_authorizations SET sku_color=? WHERE id=?` , string ([] byte { 0xff }), testAuthA )
},
"authorization overlong color" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE order_authorizations SET sku_color=? WHERE id=?` , strings . Repeat ( "色" , 81 ), testAuthA )
},
"authorization overlong size" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE order_authorizations SET sku_size=? WHERE id=?` , strings . Repeat ( "码" , 81 ), testAuthA )
},
"authorization overlong money" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE order_authorizations SET total_price_cap=? WHERE id=?` , strings . Repeat ( "1" , 30 ) + ".00" , testAuthA )
2026-08-05 01:25:23 +08:00
},
}
for name , mutate := range mutations {
t . Run ( name , func ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( time . Minute ), true )
mutate ( t , database )
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x32 }, 32 ), 30 * time . Second )
store . now = func () time . Time { return testNow }
if _ , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA }); err == nil || found {
t . Fatalf ( "ClaimNext = found %v, err %v; want closed failure" , found , err )
}
assertClaimState ( t , database , 0 , "PENDING" , "ACTIVE" )
2026-08-05 01:39:35 +08:00
for _ , table := range [] string { "purchase_attempts" , "task_claim_requests" } {
var count int
if err := database . QueryRow ( "SELECT COUNT(*) FROM " + table ). Scan ( & count ); err != nil || count != 0 {
t . Fatalf ( "%s rows after invalid candidate = %d, err %v" , table , count , err )
}
}
2026-08-05 01:25:23 +08:00
})
}
}
func TestClaimReplayRevalidatesImmutableResponseSnapshot ( t * testing . T ) {
mutations := map [ string ] func ( * testing . T , * sql . DB ){
"title" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE purchase_attempt_claims SET task_title=? WHERE task_id=?` , strings . Repeat ( "😀" , 121 ), testTaskA )
},
"goods id" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE purchase_attempt_claims SET goods_id=? WHERE task_id=?` , strings . Repeat ( "1" , 33 ), testTaskA )
},
"color" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE purchase_attempt_claims SET sku_color=? WHERE task_id=?` , strings . Repeat ( "色" , 81 ), testTaskA )
},
"size" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE purchase_attempt_claims SET sku_size=? WHERE task_id=?` , strings . Repeat ( "码" , 81 ), testTaskA )
},
"money" : func ( t * testing . T , database * sql . DB ) {
execClaimSQL ( t , database , `UPDATE purchase_attempt_claims SET total_price_cap=? WHERE task_id=?` , strings . Repeat ( "1" , 30 ) + ".00" , testTaskA )
},
}
for name , mutate := range mutations {
t . Run ( name , func ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( time . Minute ), true )
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x34 }, 32 ), 30 * time . Second )
store . now = func () time . Time { return testNow }
command := ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA }
if _ , found , err := store . ClaimNext ( context . Background (), testDeviceA , command ); err != nil || ! found {
t . Fatalf ( "initial ClaimNext = found %v, err %v" , found , err )
}
mutate ( t , database )
if _ , found , err := store . ClaimNext ( context . Background (), testDeviceA , command ); err == nil || found {
t . Fatalf ( "replay = found %v, err %v; want invalid snapshot" , found , err )
}
})
}
}
2026-08-04 22:33:16 +08:00
func TestClaimEligibilityStableOrderAndConcurrentUniqueness ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertDevice ( t , database , testDeviceB , [] byte ( "device-b" ))
// The oldest row has a mismatched snapshot and is ineligible; the next oldest valid row wins.
insertCandidate ( t , database , testTaskA , testAuthA , testNow . Add ( - 2 * time . Minute ), testNow . Add ( 10 * time . Minute ), false )
insertCandidate ( t , database , testTaskB , testAuthB , testNow . Add ( - time . Minute ), testNow . Add ( 10 * time . Minute ), true )
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x33 }, 32 ), time . Minute )
store . now = func () time . Time { return testNow }
type result struct {
response ClaimResponse
found bool
err error
}
commands := [] struct { device , session , request string }{
{ testDeviceA , testSessionA , testClaimRequestA },
{ testDeviceB , testSessionB , testClaimRequestB },
}
results := make ( chan result , 2 )
var wait sync . WaitGroup
for _ , command := range commands {
command := command
wait . Add ( 1 )
go func () {
defer wait . Done ()
response , found , err := store . ClaimNext ( context . Background (), command . device , ClaimCommand { SessionID : command . session , ClaimRequestID : command . request })
results <- result { response , found , err }
}()
}
wait . Wait ()
close ( results )
foundCount := 0
for result := range results {
if result . err != nil {
t . Fatalf ( "concurrent ClaimNext error: %v" , result . err )
}
if result . found {
foundCount ++
if result . response . Task . ID != testTaskB {
t . Fatalf ( "claimed task = %s, want stable eligible task B" , result . response . Task . ID )
}
}
}
if foundCount != 1 {
t . Fatalf ( "successful claims = %d, want 1" , foundCount )
}
var claimCount int
if err := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempt_claims" ). Scan ( & claimCount ); err != nil || claimCount != 1 {
t . Fatalf ( "claim count = %d, err %v" , claimCount , err )
}
var taskStatus , authorizationStatus string
if err := database . QueryRow ( `SELECT tasks.status, order_authorizations.status FROM tasks
JOIN order_authorizations ON order_authorizations.task_id=tasks.id WHERE tasks.id=?` , testTaskB ).
Scan ( & taskStatus , & authorizationStatus ); err != nil || taskStatus != "CLAIMED" || authorizationStatus != "CLAIMED" {
t . Fatalf ( "claimed B states = %s/%s, err %v" , taskStatus , authorizationStatus , err )
}
}
func TestClaimConcurrencyAcrossDistinctDatabasesAndStores ( t * testing . T ) {
path := filepath . ToSlash ( filepath . Join ( t . TempDir (), "shared-claim.db" ))
source := "file:" + path + "?_busy_timeout=5000&_journal_mode=WAL"
databaseA , err := sqlite . Open ( source )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func () { _ = databaseA . Close () })
if err := migrations . Up ( context . Background (), databaseA , claimMigrationDirectory ( t )); err != nil {
t . Fatal ( err )
}
databaseB , err := sqlite . Open ( source )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func () { _ = databaseB . Close () })
databaseA . SetMaxOpenConns ( 1 )
databaseB . SetMaxOpenConns ( 1 )
insertDevice ( t , databaseA , testDeviceA , [] byte ( "device-a" ))
insertDevice ( t , databaseA , testDeviceB , [] byte ( "device-b" ))
insertCandidate ( t , databaseA , testTaskA , testAuthA , testNow , testNow . Add ( 10 * time . Minute ), true )
secret := bytes . Repeat ([] byte { 0x39 }, 32 )
storeA := mustStore ( t , databaseA , secret , time . Minute )
storeB := mustStore ( t , databaseB , secret , time . Minute )
storeA . now = func () time . Time { return testNow }
storeB . now = func () time . Time { return testNow }
firstLinearized := make ( chan struct {})
releaseFirst := make ( chan struct {})
secondAtFirstWrite := make ( chan struct {})
var releaseOnce sync . Once
release := func () { releaseOnce . Do ( func () { close ( releaseFirst ) }) }
t . Cleanup ( release )
storeA . afterLinearization = func () {
close ( firstLinearized )
<- releaseFirst
}
storeB . beforeLinearization = func () {
// Reaching this hook means B has begun its own transaction and its very next
// database operation is the first-write UPDATE currently held by A.
close ( secondAtFirstWrite )
}
type result struct {
found bool
err error
}
firstResult := make ( chan result , 1 )
secondResult := make ( chan result , 1 )
go func () {
_ , found , err := storeA . ClaimNext ( context . Background (), testDeviceA , ClaimCommand {
SessionID : testSessionA , ClaimRequestID : testClaimRequestA ,
})
firstResult <- result { found : found , err : err }
}()
select {
case <- firstLinearized :
case result := <- firstResult :
t . Fatalf ( "first ClaimNext returned before holding SQLite write position: found %v, err %v" , result . found , result . err )
case <- time . After ( time . Second ):
t . Fatal ( "first ClaimNext did not reach SQLite write position" )
}
go func () {
_ , found , err := storeB . ClaimNext ( context . Background (), testDeviceB , ClaimCommand {
SessionID : testSessionB , ClaimRequestID : testClaimRequestB ,
})
secondResult <- result { found : found , err : err }
}()
select {
case <- secondAtFirstWrite :
// A still owns the SQLite write position here. B cannot have observed or
// changed claim state, so releasing A below creates deterministic contention.
case result := <- secondResult :
release ()
<- firstResult
t . Fatalf ( "second ClaimNext returned before reaching the contended first write: found %v, err %v" , result . found , result . err )
case <- time . After ( time . Second ):
release ()
<- firstResult
t . Fatal ( "second ClaimNext did not reach the contended SQLite first write" )
}
select {
case result := <- secondResult :
release ()
<- firstResult
t . Fatalf ( "second ClaimNext completed while first transaction held SQLite write position: found %v, err %v" , result . found , result . err )
default :
}
release ()
first := <- firstResult
second := <- secondResult
if first . err != nil || ! first . found {
t . Fatalf ( "first cross-database ClaimNext = found %v, err %v" , first . found , first . err )
}
if second . err != nil || second . found {
t . Fatalf ( "second cross-database ClaimNext = found %v, err %v" , second . found , second . err )
}
var attempts , claims , requestsCount , claimedRequests , emptyRequests int
queries := [] struct {
query string
value * int
}{
{ "SELECT COUNT(*) FROM purchase_attempts" , & attempts },
{ "SELECT COUNT(*) FROM purchase_attempt_claims" , & claims },
{ "SELECT COUNT(*) FROM task_claim_requests" , & requestsCount },
{ "SELECT COUNT(*) FROM task_claim_requests WHERE outcome='CLAIMED'" , & claimedRequests },
{ "SELECT COUNT(*) FROM task_claim_requests WHERE outcome='EMPTY'" , & emptyRequests },
}
for _ , query := range queries {
if err := databaseA . QueryRow ( query . query ). Scan ( query . value ); err != nil {
t . Fatal ( err )
}
}
if attempts != 1 || claims != 1 || requestsCount != 2 || claimedRequests != 1 || emptyRequests != 1 {
t . Fatalf ( "cross-database attempts/claims/requests/claimed/empty = %d/%d/%d/%d/%d" ,
attempts , claims , requestsCount , claimedRequests , emptyRequests )
}
}
func TestRenewCASReplayCapAndNoResurrection ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( 40 * time . Second ), true )
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x44 }, 32 ), 30 * time . Second )
current := testNow
store . now = func () time . Time { return current }
claim , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err != nil || ! found {
t . Fatalf ( "ClaimNext = found %v, err %v" , found , err )
}
current = testNow . Add ( 20 * time . Second )
command := RenewCommand { TaskID : testTaskA , RenewRequestID : testRenewRequestA , SessionID : testSessionA ,
AttemptID : claim . Attempt . ID , ClaimGeneration : claim . Attempt . ClaimGeneration ,
ClaimToken : claim . Attempt . ClaimToken , ExpectedLeaseExpiresAt : claim . Attempt . LeaseExpiresAt }
renewed , err := store . Renew ( context . Background (), testDeviceA , command )
if err != nil {
t . Fatalf ( "Renew: %v" , err )
}
wantCap := formatTime ( testNow . Add ( 40 * time . Second ))
if renewed . LeaseExpiresAt != wantCap {
t . Fatalf ( "renewed lease = %s, want authorization cap %s" , renewed . LeaseExpiresAt , wantCap )
}
current = testNow . Add ( 25 * time . Second )
replay , err := store . Renew ( context . Background (), testDeviceA , command )
if err != nil || ! reflect . DeepEqual ( replay , renewed ) {
t . Fatalf ( "renew replay = %#v, err %v" , replay , err )
}
changed := command
changed . ExpectedLeaseExpiresAt = renewed . LeaseExpiresAt
if _ , err := store . Renew ( context . Background (), testDeviceA , changed ); ! errors . Is ( err , ErrIdempotencyConflict ) {
t . Fatalf ( "same key different payload error = %v" , err )
}
stale := command
stale . RenewRequestID = "60000000-0000-4000-8000-000000000004"
if _ , err := store . Renew ( context . Background (), testDeviceA , stale ); ! errors . Is ( err , ErrNotCurrent ) {
t . Fatalf ( "out-of-order expected lease error = %v" , err )
}
wrongToken := command
wrongToken . RenewRequestID = testRenewRequestB
wrongToken . ClaimToken = strings . Repeat ( "0" , 64 )
if _ , err := store . Renew ( context . Background (), testDeviceA , wrongToken ); ! errors . Is ( err , ErrNotCurrent ) {
t . Fatalf ( "wrong token error = %v" , err )
}
current = testNow . Add ( 40 * time . Second ) // equality is expired; no grace and no resurrection.
expired := command
expired . RenewRequestID = "60000000-0000-4000-8000-000000000003"
expired . ExpectedLeaseExpiresAt = renewed . LeaseExpiresAt
if _ , err := store . Renew ( context . Background (), testDeviceA , expired ); ! errors . Is ( err , ErrNotCurrent ) {
t . Fatalf ( "expired renewal error = %v" , err )
}
var lease , taskStatus , attemptStatus , authorizationStatus string
if err := database . QueryRow ( `SELECT claims.lease_expires_at, tasks.status, attempts.status, authorizations.status
FROM purchase_attempt_claims claims JOIN tasks ON tasks.id=claims.task_id
JOIN purchase_attempts attempts ON attempts.id=claims.attempt_id
JOIN order_authorizations authorizations ON authorizations.id=claims.authorization_id` ).
Scan ( & lease , & taskStatus , & attemptStatus , & authorizationStatus ); err != nil {
t . Fatal ( err )
}
if lease != wantCap || taskStatus != "CLAIMED" || attemptStatus != "CLAIMED" || authorizationStatus != "CLAIMED" {
t . Fatalf ( "renew changed business state: lease=%s task=%s attempt=%s auth=%s" , lease , taskStatus , attemptStatus , authorizationStatus )
}
if _ , err := database . Exec ( `UPDATE device_credentials SET status='REVOKED', revoked_at=? WHERE device_id=?` , formatTime ( current ), testDeviceA ); err != nil {
t . Fatal ( err )
}
revoked := expired
revoked . RenewRequestID = "60000000-0000-4000-8000-000000000005"
if _ , err := store . Renew ( context . Background (), testDeviceA , revoked ); ! errors . Is ( err , ErrDeviceInactive ) {
t . Fatalf ( "renew after revocation error = %v" , err )
}
}
func TestRenewRequiresPairedBusinessStateAndExactTaskVersion ( t * testing . T ) {
tests := [] struct {
name string
mutate func ( * testing . T , * sql . DB , ClaimResponse )
wantError bool
}{
{ "claimed exact version" , func ( * testing . T , * sql . DB , ClaimResponse ) {}, false },
{ "ordering exact next version" , func ( t * testing . T , database * sql . DB , claim ClaimResponse ) {
if _ , err := database . Exec ( "UPDATE tasks SET status='ORDERING',version=version+1 WHERE id=?" , testTaskA ); err != nil {
t . Fatal ( err )
}
if _ , err := database . Exec ( "UPDATE purchase_attempts SET status='ORDERING' WHERE id=?" , claim . Attempt . ID ); err != nil {
t . Fatal ( err )
}
}, false },
{ "claimed version drift" , func ( t * testing . T , database * sql . DB , _ ClaimResponse ) {
if _ , err := database . Exec ( "UPDATE tasks SET version=version+1 WHERE id=?" , testTaskA ); err != nil {
t . Fatal ( err )
}
}, true },
{ "ordering version drift" , func ( t * testing . T , database * sql . DB , claim ClaimResponse ) {
if _ , err := database . Exec ( "UPDATE tasks SET status='ORDERING',version=version+2 WHERE id=?" , testTaskA ); err != nil {
t . Fatal ( err )
}
if _ , err := database . Exec ( "UPDATE purchase_attempts SET status='ORDERING' WHERE id=?" , claim . Attempt . ID ); err != nil {
t . Fatal ( err )
}
}, true },
{ "task ordering attempt claimed" , func ( t * testing . T , database * sql . DB , _ ClaimResponse ) {
if _ , err := database . Exec ( "UPDATE tasks SET status='ORDERING',version=version+1 WHERE id=?" , testTaskA ); err != nil {
t . Fatal ( err )
}
}, true },
{ "task claimed attempt ordering" , func ( t * testing . T , database * sql . DB , claim ClaimResponse ) {
if _ , err := database . Exec ( "UPDATE purchase_attempts SET status='ORDERING' WHERE id=?" , claim . Attempt . ID ); err != nil {
t . Fatal ( err )
}
}, true },
}
for index , test := range tests {
t . Run ( test . name , func ( t * testing . T ) {
database , store , claim := claimedRenewFixture ( t , byte ( 0x50 + index ))
test . mutate ( t , database , claim )
_ , err := store . Renew ( context . Background (), testDeviceA , renewCommandFor ( claim , testRenewRequestA ))
if test . wantError {
if ! errors . Is ( err , ErrNotCurrent ) {
t . Fatalf ( "Renew error = %v, want ErrNotCurrent" , err )
}
var lease string
var renewals int
if scanErr := database . QueryRow ( "SELECT lease_expires_at FROM purchase_attempt_claims WHERE attempt_id=?" , claim . Attempt . ID ). Scan ( & lease ); scanErr != nil {
t . Fatal ( scanErr )
}
if scanErr := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempt_lease_renewals" ). Scan ( & renewals ); scanErr != nil {
t . Fatal ( scanErr )
}
if lease != claim . Attempt . LeaseExpiresAt || renewals != 0 {
t . Fatalf ( "rejected renew changed lease/rows = %s/%d" , lease , renewals )
}
return
}
if err != nil {
t . Fatalf ( "Renew valid state: %v" , err )
}
})
}
}
func TestConcurrentRenewCASUsesSQLiteNotOneStoreGate ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( 10 * time . Minute ), true )
secret := bytes . Repeat ([] byte { 0x48 }, 32 )
storeA := mustStore ( t , database , secret , time . Minute )
storeA . now = func () time . Time { return testNow }
claim , found , err := storeA . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err != nil || ! found {
t . Fatalf ( "ClaimNext = found %v, err %v" , found , err )
}
storeB := mustStore ( t , database , secret , time . Minute )
renewNow := testNow . Add ( 10 * time . Second )
storeA . now = func () time . Time { return renewNow }
storeB . now = func () time . Time { return renewNow }
base := RenewCommand { TaskID : testTaskA , SessionID : testSessionA , AttemptID : claim . Attempt . ID ,
ClaimGeneration : claim . Attempt . ClaimGeneration , ClaimToken : claim . Attempt . ClaimToken ,
ExpectedLeaseExpiresAt : claim . Attempt . LeaseExpiresAt }
commands := [] RenewCommand { base , base }
commands [ 0 ]. RenewRequestID = testRenewRequestA
commands [ 1 ]. RenewRequestID = testRenewRequestB
type result struct { err error }
results := make ( chan result , 2 )
var wait sync . WaitGroup
for index , claimStore := range [] * Store { storeA , storeB } {
index , claimStore := index , claimStore
wait . Add ( 1 )
go func () {
defer wait . Done ()
_ , err := claimStore . Renew ( context . Background (), testDeviceA , commands [ index ])
results <- result { err : err }
}()
}
wait . Wait ()
close ( results )
successes , stale := 0 , 0
for result := range results {
switch {
case result . err == nil :
successes ++
case errors . Is ( result . err , ErrNotCurrent ):
stale ++
default :
t . Fatalf ( "concurrent Renew error = %v" , result . err )
}
}
var renewalCount int
if err := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempt_lease_renewals" ). Scan ( & renewalCount ); err != nil {
t . Fatal ( err )
}
if successes != 1 || stale != 1 || renewalCount != 1 {
t . Fatalf ( "concurrent renew success/stale/rows = %d/%d/%d" , successes , stale , renewalCount )
}
}
func TestRenewRevocationLinearizationBothOrders ( t * testing . T ) {
t . Run ( "revocation first" , func ( t * testing . T ) {
database , store , claim := claimedRenewFixture ( t , 0x49 )
if _ , err := database . Exec ( `UPDATE device_credentials SET status='REVOKED', revoked_at=? WHERE device_id=?` , formatTime ( testNow . Add ( time . Second )), testDeviceA ); err != nil {
t . Fatal ( err )
}
command := renewCommandFor ( claim , testRenewRequestA )
if _ , err := store . Renew ( context . Background (), testDeviceA , command ); ! errors . Is ( err , ErrDeviceInactive ) {
t . Fatalf ( "Renew after revocation error = %v" , err )
}
})
t . Run ( "renew write position first" , func ( t * testing . T ) {
database , store , claim := claimedRenewFixture ( t , 0x4a )
linearized := make ( chan struct {})
release := make ( chan struct {})
store . afterLinearization = func () { close ( linearized ); <- release }
renewResult := make ( chan error , 1 )
go func () {
_ , err := store . Renew ( context . Background (), testDeviceA , renewCommandFor ( claim , testRenewRequestA ))
renewResult <- err
}()
<- linearized
revocationStarted := make ( chan struct {})
revocationResult := make ( chan error , 1 )
go func () {
close ( revocationStarted )
_ , err := database . Exec ( `UPDATE device_credentials SET status='REVOKED', revoked_at=?
WHERE device_id=? AND status='ACTIVE'` , formatTime ( testNow . Add ( 2 * time . Second )), testDeviceA )
revocationResult <- err
}()
<- revocationStarted
close ( release )
if err := <- renewResult ; err != nil {
t . Fatalf ( "renew holding first write position: %v" , err )
}
if err := <- revocationResult ; err != nil {
t . Fatalf ( "revocation after renew: %v" , err )
}
var renewals int
if err := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempt_lease_renewals" ). Scan ( & renewals ); err != nil || renewals != 1 {
t . Fatalf ( "renewal rows = %d, err %v" , renewals , err )
}
})
}
func TestStartupValidatesClosedClaimStorageTypesAndStatuses ( t * testing . T ) {
for _ , test := range [] struct {
name string
mutate func ( * testing . T , * sql . DB )
}{
{ "text nonce" , func ( t * testing . T , database * sql . DB ) {
if _ , err := database . Exec ( `UPDATE purchase_attempt_claims
SET claim_nonce=CAST('12345678901234567890123456789012' AS TEXT)` ); err != nil {
t . Fatal ( err )
}
}},
{ "invalid attempt status" , func ( t * testing . T , database * sql . DB ) {
if _ , err := database . Exec ( "UPDATE purchase_attempts SET status='CORRUPT'" ); err != nil {
t . Fatal ( err )
}
}},
} {
t . Run ( test . name , func ( t * testing . T ) {
database := openClaimTestDatabase ( t )
database . SetMaxOpenConns ( 1 )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( 10 * time . Minute ), true )
secret := bytes . Repeat ([] byte { 0x4b }, 32 )
store := mustStore ( t , database , secret , time . Minute )
store . now = func () time . Time { return testNow }
claim , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err != nil || ! found {
t . Fatalf ( "ClaimNext = found %v, err %v" , found , err )
}
if _ , err := database . Exec ( "UPDATE purchase_attempt_claims SET closed_at=? WHERE attempt_id=?" , formatTime ( testNow . Add ( 2 * time . Minute )), claim . Attempt . ID ); err != nil {
t . Fatal ( err )
}
if _ , err := NewStore ( database , secret , time . Minute ); err != nil {
t . Fatalf ( "valid closed claim rejected: %v" , err )
}
if _ , err := database . Exec ( "PRAGMA ignore_check_constraints=ON" ); err != nil {
t . Fatal ( err )
}
test . mutate ( t , database )
if _ , err := NewStore ( database , secret , time . Minute ); err == nil {
t . Fatal ( "NewStore accepted corrupted closed claim storage" )
}
})
}
}
func TestStartupRejectsClaimAttemptGenerationCorruption ( t * testing . T ) {
database := openClaimTestDatabase ( t )
database . SetMaxOpenConns ( 1 )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( 10 * time . Minute ), true )
secret := bytes . Repeat ([] byte { 0x4c }, 32 )
store := mustStore ( t , database , secret , time . Minute )
store . now = func () time . Time { return testNow }
claim , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err != nil || ! found {
t . Fatalf ( "ClaimNext = found %v, err %v" , found , err )
}
var nonce [] byte
if err := database . QueryRow ( "SELECT claim_nonce FROM purchase_attempt_claims WHERE attempt_id=?" , claim . Attempt . ID ). Scan ( & nonce ); err != nil {
t . Fatal ( err )
}
corruptGeneration := claim . Attempt . ClaimGeneration + 1
corruptToken := deriveToken ( secret , testDeviceA , testTaskA , testAuthA , claim . Attempt . ID , corruptGeneration , nonce )
if _ , err := database . Exec ( "PRAGMA foreign_keys=OFF" ); err != nil {
t . Fatal ( err )
}
if _ , err := database . Exec ( `UPDATE purchase_attempt_claims SET claim_generation=?,claim_token_sha256=? WHERE attempt_id=?` ,
corruptGeneration , tokenHash ( corruptToken ), claim . Attempt . ID ); err != nil {
t . Fatalf ( "inject generation corruption: %v" , err )
}
if _ , err := NewStore ( database , secret , time . Minute ); err == nil {
t . Fatal ( "NewStore accepted claim generation different from its attempt" )
}
}
func TestRevocationLinearizesBeforeOrAfterClaim ( t * testing . T ) {
t . Run ( "revocation first" , func ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( time . Minute ), true )
if _ , err := database . Exec ( `UPDATE device_credentials SET status='REVOKED', revoked_at=? WHERE device_id=?` , formatTime ( testNow ), testDeviceA ); err != nil {
t . Fatal ( err )
}
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x55 }, 32 ), 30 * time . Second )
if _ , _ , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA }); ! errors . Is ( err , ErrDeviceInactive ) {
t . Fatalf ( "ClaimNext error = %v, want inactive" , err )
}
assertClaimState ( t , database , 0 , "PENDING" , "ACTIVE" )
})
t . Run ( "claim write position first" , func ( t * testing . T ) {
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( time . Minute ), true )
store := mustStore ( t , database , bytes . Repeat ([] byte { 0x66 }, 32 ), 30 * time . Second )
store . now = func () time . Time { return testNow }
linearized := make ( chan struct {})
release := make ( chan struct {})
store . afterLinearization = func () { close ( linearized ); <- release }
claimResult := make ( chan error , 1 )
go func () {
_ , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err == nil && ! found {
err = errors . New ( "claim unexpectedly empty" )
}
claimResult <- err
}()
<- linearized
revocationStarted := make ( chan struct {})
revocationResult := make ( chan error , 1 )
go func () {
close ( revocationStarted )
_ , err := database . Exec ( `UPDATE device_credentials SET status='REVOKED', revoked_at=? WHERE device_id=? AND status='ACTIVE'` , formatTime ( testNow . Add ( time . Second )), testDeviceA )
revocationResult <- err
}()
<- revocationStarted
close ( release )
if err := <- claimResult ; err != nil {
t . Fatalf ( "claim holding first write position: %v" , err )
}
if err := <- revocationResult ; err != nil {
t . Fatalf ( "revocation after claim: %v" , err )
}
assertClaimState ( t , database , 1 , "CLAIMED" , "CLAIMED" )
})
}
func TestTokenDomainSeparationAndDeviceSecretIsolation ( t * testing . T ) {
secret := bytes . Repeat ([] byte { 0x77 }, 32 )
nonce := bytes . Repeat ([] byte { 0x88 }, 32 )
base := deriveToken ( secret , testDeviceA , testTaskA , testAuthA , "70000000-0000-4000-8000-000000000001" , 1 , nonce )
variants := [][] byte {
deriveToken ( secret , testDeviceB , testTaskA , testAuthA , "70000000-0000-4000-8000-000000000001" , 1 , nonce ),
deriveToken ( secret , testDeviceA , testTaskB , testAuthA , "70000000-0000-4000-8000-000000000001" , 1 , nonce ),
deriveToken ( secret , testDeviceA , testTaskA , testAuthB , "70000000-0000-4000-8000-000000000001" , 1 , nonce ),
deriveToken ( secret , testDeviceA , testTaskA , testAuthA , "70000000-0000-4000-8000-000000000002" , 1 , nonce ),
deriveToken ( secret , testDeviceA , testTaskA , testAuthA , "70000000-0000-4000-8000-000000000001" , 2 , nonce ),
}
for index , variant := range variants {
if matchingHash ( base , variant ) {
t . Fatalf ( "token variant %d was not domain separated" , index )
}
}
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , secret )
if _ , err := NewStore ( database , secret , time . Minute ); err == nil {
t . Fatal ( "NewStore accepted a key equal to a device token" )
}
}
func openClaimTestDatabase ( t * testing . T ) * sql . DB {
t . Helper ()
path := filepath . ToSlash ( filepath . Join ( t . TempDir (), "claim.db" ))
database , err := sqlite . Open ( "file:" + path + "?_busy_timeout=5000&_journal_mode=WAL" )
if err != nil {
t . Fatalf ( "open database: %v" , err )
}
t . Cleanup ( func () { _ = database . Close () })
if err := migrations . Up ( context . Background (), database , claimMigrationDirectory ( t )); err != nil {
t . Fatalf ( "migrate database: %v" , err )
}
return database
}
func claimMigrationDirectory ( t * testing . T ) string {
t . Helper ()
_ , file , _ , ok := runtime . Caller ( 0 )
if ! ok {
t . Fatal ( "locate test file" )
}
return filepath . Join ( filepath . Dir ( file ), ".." , ".." , "migrations" )
}
func mustStore ( t * testing . T , database * sql . DB , secret [] byte , ttl time . Duration ) * Store {
t . Helper ()
store , err := NewStore ( database , secret , ttl )
if err != nil {
t . Fatalf ( "NewStore: %v" , err )
}
return store
}
func claimedRenewFixture ( t * testing . T , secretByte byte ) ( * sql . DB , * Store , ClaimResponse ) {
t . Helper ()
database := openClaimTestDatabase ( t )
insertDevice ( t , database , testDeviceA , [] byte ( "device-a" ))
insertCandidate ( t , database , testTaskA , testAuthA , testNow , testNow . Add ( 10 * time . Minute ), true )
store := mustStore ( t , database , bytes . Repeat ([] byte { secretByte }, 32 ), time . Minute )
store . now = func () time . Time { return testNow }
claim , found , err := store . ClaimNext ( context . Background (), testDeviceA , ClaimCommand { SessionID : testSessionA , ClaimRequestID : testClaimRequestA })
if err != nil || ! found {
t . Fatalf ( "ClaimNext = found %v, err %v" , found , err )
}
store . now = func () time . Time { return testNow . Add ( 10 * time . Second ) }
return database , store , claim
}
func renewCommandFor ( claim ClaimResponse , requestID string ) RenewCommand {
return RenewCommand { TaskID : testTaskA , RenewRequestID : requestID , SessionID : testSessionA ,
AttemptID : claim . Attempt . ID , ClaimGeneration : claim . Attempt . ClaimGeneration ,
ClaimToken : claim . Attempt . ClaimToken , ExpectedLeaseExpiresAt : claim . Attempt . LeaseExpiresAt }
}
func insertDevice ( t * testing . T , database * sql . DB , deviceID string , token [] byte ) {
t . Helper ()
digest := sha256 . Sum256 ( token )
if _ , err := database . Exec ( `INSERT INTO device_credentials
(device_id,display_name,token_sha256,status,created_at,revoked_at)
VALUES (?, ?, ?, 'ACTIVE', ?, NULL)` , deviceID , "test device" , digest [:], formatTime ( testNow . Add ( - time . Hour ))); err != nil {
t . Fatalf ( "insert device: %v" , err )
}
}
func insertCandidate ( t * testing . T , database * sql . DB , taskID , authorizationID string , createdAt , expiresAt time . Time , snapshotMatches bool ) {
t . Helper ()
if _ , err := database . Exec ( `INSERT INTO tasks
(id,source,title,goods_id,sku_color,sku_size,quantity,max_total_price,status,version,created_at,updated_at)
VALUES (?, 'MANUAL', '测试商品', '937122477375', '黑色', 'M', 2, '30.00', 'PENDING', 2, ?, ?)` ,
taskID , formatTime ( createdAt ), formatTime ( createdAt )); err != nil {
t . Fatalf ( "insert task: %v" , err )
}
color := "黑色"
if ! snapshotMatches {
color = "白色"
}
if _ , err := database . Exec ( `INSERT INTO order_authorizations
(id,task_id,task_version,start_key,goods_id,sku_color,sku_size,quantity,total_price_cap,status,created_by,created_at,expires_at)
VALUES (?, ?, 2, ?, '937122477375', ?, 'M', 2, '30.00', 'ACTIVE', 'admin', ?, ?)` ,
authorizationID , taskID , authorizationID , color , formatTime ( createdAt ), formatTime ( expiresAt )); err != nil {
t . Fatalf ( "insert authorization: %v" , err )
}
}
2026-08-05 01:25:23 +08:00
func execClaimSQL ( t * testing . T , database * sql . DB , statement string , arguments ... any ) {
t . Helper ()
if _ , err := database . Exec ( statement , arguments ... ); err != nil {
t . Fatalf ( "execute claim test SQL: %v" , err )
}
}
2026-08-04 22:33:16 +08:00
func assertClaimState ( t * testing . T , database * sql . DB , wantClaims int , wantTaskStatus , wantAuthorizationStatus string ) {
t . Helper ()
var count int
if err := database . QueryRow ( "SELECT COUNT(*) FROM purchase_attempt_claims" ). Scan ( & count ); err != nil || count != wantClaims {
t . Fatalf ( "claim count = %d, err %v, want %d" , count , err , wantClaims )
}
var taskStatus , authorizationStatus string
if err := database . QueryRow ( `SELECT tasks.status, order_authorizations.status FROM tasks
JOIN order_authorizations ON order_authorizations.task_id=tasks.id
WHERE tasks.id=?` , testTaskA ). Scan ( & taskStatus , & authorizationStatus ); err != nil {
t . Fatal ( err )
}
if taskStatus != wantTaskStatus || authorizationStatus != wantAuthorizationStatus {
t . Fatalf ( "states = %s/%s, want %s/%s" , taskStatus , authorizationStatus , wantTaskStatus , wantAuthorizationStatus )
}
}
func assertNoPlaintextTokenColumnOrValue ( t * testing . T , database * sql . DB , token string ) {
t . Helper ()
rows , err := database . Query ( "PRAGMA table_info(purchase_attempt_claims)" )
if err != nil {
t . Fatal ( err )
}
defer rows . Close ()
for rows . Next () {
var cid , notNull , primaryKey int
var name , kind string
var defaultValue any
if err := rows . Scan ( & cid , & name , & kind , & notNull , & defaultValue , & primaryKey ); err != nil {
t . Fatal ( err )
}
if name == "claim_token" {
t . Fatal ( "schema contains a plaintext claim_token column" )
}
}
decoded , _ := hex . DecodeString ( token )
var nonce , storedHash [] byte
if err := database . QueryRow ( "SELECT claim_nonce, claim_token_sha256 FROM purchase_attempt_claims" ). Scan ( & nonce , & storedHash ); err != nil {
t . Fatal ( err )
}
if bytes . Equal ( nonce , decoded ) || bytes . Equal ( storedHash , decoded ) || len ( nonce ) != 32 || len ( storedHash ) != 32 {
t . Fatal ( "database contains plaintext token or malformed token metadata" )
}
}