Skip to content

Commit a4a15c4

Browse files
authored
test: cover object version listing and wallet operations
1 parent ea6de2d commit a4a15c4

3 files changed

Lines changed: 265 additions & 61 deletions

File tree

internal/admin/api_wallet_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,61 @@ func TestHandleAPIWalletApprove_RejectsAmountField(t *testing.T) {
307307
}
308308
}
309309

310+
func TestHandleAPIWalletOperations(t *testing.T) {
311+
srv, repos := newWalletOperationTestServer(t)
312+
ctx := context.Background()
313+
for _, seed := range []repository.CreateWalletOperationInput{
314+
{Type: model.WalletOperationTypeFund, ClientRequestID: "req-1", Amount: "100"},
315+
{Type: model.WalletOperationTypeWithdraw, ClientRequestID: "req-2", Amount: "50"},
316+
{Type: model.WalletOperationTypeApprove, ClientRequestID: "req-3", Amount: "0"},
317+
} {
318+
if _, _, err := repos.WalletOperations.CreateOrGet(ctx, seed); err != nil {
319+
t.Fatalf("CreateOrGet %q: %v", seed.ClientRequestID, err)
320+
}
321+
}
322+
323+
tests := []struct {
324+
name string
325+
query string
326+
wantStatus int
327+
wantIDs []string
328+
}{
329+
{name: "default limit returns recent operations", wantStatus: http.StatusOK, wantIDs: []string{"req-3", "req-2", "req-1"}},
330+
{name: "custom limit returns most recent operations", query: "?limit=2", wantStatus: http.StatusOK, wantIDs: []string{"req-3", "req-2"}},
331+
{name: "invalid limit", query: "?limit=abc", wantStatus: http.StatusBadRequest},
332+
{name: "zero limit", query: "?limit=0", wantStatus: http.StatusBadRequest},
333+
{name: "negative limit", query: "?limit=-1", wantStatus: http.StatusBadRequest},
334+
}
335+
336+
for _, tt := range tests {
337+
t.Run(tt.name, func(t *testing.T) {
338+
req := httptest.NewRequest(http.MethodGet, "/api/v1/wallet/operations"+tt.query, nil)
339+
rr := httptest.NewRecorder()
340+
341+
srv.handleAPIWalletOperations(rr, req)
342+
343+
if rr.Code != tt.wantStatus {
344+
t.Fatalf("status = %d, want %d, body=%s", rr.Code, tt.wantStatus, rr.Body.String())
345+
}
346+
if tt.wantStatus != http.StatusOK {
347+
return
348+
}
349+
var resp walletOperationsResponse
350+
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
351+
t.Fatalf("Unmarshal operation response: %v", err)
352+
}
353+
if len(resp.Operations) != len(tt.wantIDs) {
354+
t.Fatalf("operations count = %d, want %d", len(resp.Operations), len(tt.wantIDs))
355+
}
356+
for i, wantID := range tt.wantIDs {
357+
if resp.Operations[i].ClientRequestID != wantID {
358+
t.Fatalf("operations[%d].client_request_id = %q, want %q", i, resp.Operations[i].ClientRequestID, wantID)
359+
}
360+
}
361+
})
362+
}
363+
}
364+
310365
type walletOperationAPIHandler struct {
311366
name string
312367
path string

internal/db/repository/object_repo_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,68 @@ func TestObjectRepo_ListVersionsByBucketOrdersAndMarksCurrent(t *testing.T) {
962962
}
963963
}
964964

965+
func TestObjectRepo_ListVersionsByKey(t *testing.T) {
966+
db := testDB(t)
967+
repos := repository.NewRepositories(db)
968+
ctx := context.Background()
969+
bucket := seedBucket(t, db, "version-list-key-bucket")
970+
otherBucket := seedBucket(t, db, "version-list-key-other-bucket")
971+
972+
oldVersion := newObjectVersion(bucket.ID, "file.txt", "01J00000000000000000002001", 10)
973+
if _, err := repos.Objects.CreateVersionAndSetCurrent(ctx, oldVersion); err != nil {
974+
t.Fatalf("create old version: %v", err)
975+
}
976+
middleVersion := newObjectVersion(bucket.ID, "file.txt", "01J00000000000000000002002", 20)
977+
if _, err := repos.Objects.CreateVersionAndSetCurrent(ctx, middleVersion); err != nil {
978+
t.Fatalf("create middle version: %v", err)
979+
}
980+
currentVersion := newObjectVersion(bucket.ID, "file.txt", "01J00000000000000000002003", 30)
981+
if _, err := repos.Objects.CreateVersionAndSetCurrent(ctx, currentVersion); err != nil {
982+
t.Fatalf("create current version: %v", err)
983+
}
984+
if _, err := repos.Objects.CreateVersionAndSetCurrent(ctx, newObjectVersion(bucket.ID, "other.txt", "01J00000000000000000002004", 40)); err != nil {
985+
t.Fatalf("create other key version: %v", err)
986+
}
987+
if _, err := repos.Objects.CreateVersionAndSetCurrent(ctx, newObjectVersion(otherBucket.ID, "file.txt", "01J00000000000000000002005", 50)); err != nil {
988+
t.Fatalf("create other bucket version: %v", err)
989+
}
990+
991+
rows, err := repos.Objects.ListVersionsByKey(ctx, bucket.ID, "file.txt", "", 10)
992+
if err != nil {
993+
t.Fatalf("ListVersionsByKey: %v", err)
994+
}
995+
if len(rows) != 3 {
996+
t.Fatalf("rows len = %d, want 3", len(rows))
997+
}
998+
for i, row := range rows {
999+
if row.BucketID != bucket.ID || row.Key != "file.txt" {
1000+
t.Fatalf("row %d = bucket:%d key:%q, want target bucket/file.txt", i, row.BucketID, row.Key)
1001+
}
1002+
}
1003+
if rows[0].VersionID != currentVersion.VersionID || !rows[0].IsCurrent {
1004+
t.Fatalf("first row = %#v, want current version %s", rows[0], currentVersion.VersionID)
1005+
}
1006+
if rows[1].VersionID != middleVersion.VersionID || rows[2].VersionID != oldVersion.VersionID {
1007+
t.Fatalf("ordered rows = %s/%s/%s, want current/middle/old", rows[0].VersionID, rows[1].VersionID, rows[2].VersionID)
1008+
}
1009+
1010+
page, err := repos.Objects.ListVersionsByKey(ctx, bucket.ID, "file.txt", currentVersion.VersionID, 10)
1011+
if err != nil {
1012+
t.Fatalf("ListVersionsByKey marker: %v", err)
1013+
}
1014+
if len(page) != 2 || page[0].VersionID != middleVersion.VersionID || page[1].VersionID != oldVersion.VersionID {
1015+
t.Fatalf("marker page = %#v, want middle then old", page)
1016+
}
1017+
1018+
limited, err := repos.Objects.ListVersionsByKey(ctx, bucket.ID, "file.txt", "", 2)
1019+
if err != nil {
1020+
t.Fatalf("ListVersionsByKey limit: %v", err)
1021+
}
1022+
if len(limited) != 2 || limited[0].VersionID != currentVersion.VersionID || limited[1].VersionID != middleVersion.VersionID {
1023+
t.Fatalf("limited rows = %#v, want current then middle", limited)
1024+
}
1025+
}
1026+
9651027
func TestObjectRepo_CreateDeleteMarkerHidesCurrentObjectButKeepsVersionHistory(t *testing.T) {
9661028
db := testDB(t)
9671029
repos := repository.NewRepositories(db)

internal/worker/wallet_operation_runner_test.go

Lines changed: 148 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -351,77 +351,146 @@ func TestWalletOperationRunner_ReceiptLookupTimeoutDoesNotBlockRunner(t *testing
351351
}
352352

353353
func TestWalletOperationRunner_BroadcastTimeoutDoesNotFailOperation(t *testing.T) {
354-
db := testutil.NewTestDB(t)
355-
repos := repository.NewRepositories(db)
356-
ctx := context.Background()
357-
op, _, err := repos.WalletOperations.CreateOrGet(ctx, repository.CreateWalletOperationInput{
358-
Type: model.WalletOperationTypeFund,
359-
ClientRequestID: "fund-broadcast-timeout",
360-
Amount: "100",
361-
})
362-
if err != nil {
363-
t.Fatalf("CreateOrGet: %v", err)
364-
}
354+
for _, tc := range []struct {
355+
name string
356+
opType model.WalletOperationType
357+
amount string
358+
operator *fakeWalletOperator
359+
}{
360+
{name: "fund", opType: model.WalletOperationTypeFund, amount: "100", operator: &fakeWalletOperator{blockFund: true}},
361+
{name: "withdraw", opType: model.WalletOperationTypeWithdraw, amount: "100", operator: &fakeWalletOperator{blockWithdraw: true}},
362+
{name: "approve", opType: model.WalletOperationTypeApprove, amount: "0", operator: &fakeWalletOperator{blockApprove: true}},
363+
} {
364+
t.Run(tc.name, func(t *testing.T) {
365+
db := testutil.NewTestDB(t)
366+
repos := repository.NewRepositories(db)
367+
ctx := context.Background()
368+
op, _, err := repos.WalletOperations.CreateOrGet(ctx, repository.CreateWalletOperationInput{
369+
Type: tc.opType,
370+
ClientRequestID: string(tc.opType) + "-broadcast-timeout",
371+
Amount: tc.amount,
372+
})
373+
if err != nil {
374+
t.Fatalf("CreateOrGet: %v", err)
375+
}
365376

366-
operator := &fakeWalletOperator{blockFund: true}
367-
runner := NewWalletOperationRunner(repos, operator, nil, time.Millisecond, nil, WithWalletOperationTimeouts(time.Millisecond, 0))
377+
runner := NewWalletOperationRunner(repos, tc.operator, nil, time.Millisecond, nil, WithWalletOperationTimeouts(time.Millisecond, 0))
368378

369-
started := time.Now()
370-
runner.runOnce(ctx)
371-
if time.Since(started) > time.Second {
372-
t.Fatal("runOnce did not return after broadcast timeout")
373-
}
379+
started := time.Now()
380+
runner.runOnce(ctx)
381+
if time.Since(started) > time.Second {
382+
t.Fatal("runOnce did not return after broadcast timeout")
383+
}
374384

375-
got, err := repos.WalletOperations.GetByID(ctx, op.ID)
376-
if err != nil {
377-
t.Fatalf("GetByID: %v", err)
378-
}
379-
if got.Status != model.WalletOperationStatusRunning {
380-
t.Fatalf("status = %q, want running until lease expiry", got.Status)
381-
}
382-
if got.TxHash != nil {
383-
t.Fatalf("tx_hash = %v, want nil", got.TxHash)
385+
got, err := repos.WalletOperations.GetByID(ctx, op.ID)
386+
if err != nil {
387+
t.Fatalf("GetByID: %v", err)
388+
}
389+
if got.Status != model.WalletOperationStatusRunning {
390+
t.Fatalf("status = %q, want running until lease expiry", got.Status)
391+
}
392+
if got.TxHash != nil {
393+
t.Fatalf("tx_hash = %v, want nil", got.TxHash)
394+
}
395+
})
384396
}
385397
}
386398

387399
func TestWalletOperationRunner_RemainsHealthyWhileBroadcasting(t *testing.T) {
388-
db := testutil.NewTestDB(t)
389-
repos := repository.NewRepositories(db)
390-
ctx, cancel := context.WithCancel(context.Background())
391-
defer cancel()
400+
for _, tc := range []struct {
401+
name string
402+
opType model.WalletOperationType
403+
amount string
404+
operator func(started, release chan struct{}) *fakeWalletOperator
405+
}{
406+
{
407+
name: "fund",
408+
opType: model.WalletOperationTypeFund,
409+
amount: "100",
410+
operator: func(started, release chan struct{}) *fakeWalletOperator {
411+
return &fakeWalletOperator{
412+
fundHash: common.HexToHash("0x123").Hex(),
413+
onFund: func(context.Context) {
414+
close(started)
415+
<-release
416+
},
417+
}
418+
},
419+
},
420+
{
421+
name: "withdraw",
422+
opType: model.WalletOperationTypeWithdraw,
423+
amount: "100",
424+
operator: func(started, release chan struct{}) *fakeWalletOperator {
425+
return &fakeWalletOperator{
426+
withdrawHash: common.HexToHash("0x123").Hex(),
427+
onWithdraw: func(context.Context) {
428+
close(started)
429+
<-release
430+
},
431+
}
432+
},
433+
},
434+
{
435+
name: "approve",
436+
opType: model.WalletOperationTypeApprove,
437+
amount: "0",
438+
operator: func(started, release chan struct{}) *fakeWalletOperator {
439+
return &fakeWalletOperator{
440+
approveHash: common.HexToHash("0x123").Hex(),
441+
onApprove: func(context.Context) {
442+
close(started)
443+
<-release
444+
},
445+
}
446+
},
447+
},
448+
} {
449+
t.Run(tc.name, func(t *testing.T) {
450+
db := testutil.NewTestDB(t)
451+
repos := repository.NewRepositories(db)
452+
ctx, cancel := context.WithCancel(context.Background())
453+
defer cancel()
392454

393-
if _, _, err := repos.WalletOperations.CreateOrGet(ctx, repository.CreateWalletOperationInput{
394-
Type: model.WalletOperationTypeFund,
395-
ClientRequestID: "fund-slow-broadcast",
396-
Amount: "100",
397-
}); err != nil {
398-
t.Fatalf("CreateOrGet: %v", err)
399-
}
455+
if _, _, err := repos.WalletOperations.CreateOrGet(ctx, repository.CreateWalletOperationInput{
456+
Type: tc.opType,
457+
ClientRequestID: string(tc.opType) + "-slow-broadcast",
458+
Amount: tc.amount,
459+
}); err != nil {
460+
t.Fatalf("CreateOrGet: %v", err)
461+
}
400462

401-
started := make(chan struct{})
402-
release := make(chan struct{})
403-
operator := &fakeWalletOperator{
404-
fundHash: common.HexToHash("0x123").Hex(),
405-
onFund: func(context.Context) {
406-
close(started)
407-
<-release
408-
},
409-
}
410-
runner := NewWalletOperationRunner(repos, operator, nil, time.Nanosecond, nil)
463+
started := make(chan struct{})
464+
release := make(chan struct{})
465+
var releaseOnce sync.Once
466+
defer releaseOnce.Do(func() { close(release) })
467+
468+
operator := tc.operator(started, release)
469+
runner := NewWalletOperationRunner(repos, operator, nil, time.Nanosecond, nil)
470+
471+
done := make(chan struct{})
472+
go func() {
473+
runner.runOnce(ctx)
474+
close(done)
475+
}()
476+
select {
477+
case <-started:
478+
case <-time.After(time.Second):
479+
t.Fatal("wallet broadcast did not start")
480+
}
411481

412-
done := make(chan struct{})
413-
go func() {
414-
runner.runOnce(ctx)
415-
close(done)
416-
}()
417-
<-started
482+
if !runner.Healthy() {
483+
t.Fatal("runner is unhealthy during an active wallet broadcast")
484+
}
418485

419-
if !runner.Healthy() {
420-
t.Fatal("runner is unhealthy during an active wallet broadcast")
486+
releaseOnce.Do(func() { close(release) })
487+
select {
488+
case <-done:
489+
case <-time.After(time.Second):
490+
t.Fatal("runOnce did not finish after releasing broadcast")
491+
}
492+
})
421493
}
422-
423-
close(release)
424-
<-done
425494
}
426495

427496
func TestWalletOperationRunner_RecoversSubmittedAndMarksExpiredRunningUnknown(t *testing.T) {
@@ -512,7 +581,11 @@ type fakeWalletOperator struct {
512581
withdrawAmount *big.Int
513582
approveCalled bool
514583
onFund func(context.Context)
584+
onWithdraw func(context.Context)
585+
onApprove func(context.Context)
515586
blockFund bool
587+
blockWithdraw bool
588+
blockApprove bool
516589
fundErr error
517590
withdrawErr error
518591
approveErr error
@@ -533,16 +606,30 @@ func (f *fakeWalletOperator) FundUSDFC(ctx context.Context, amount *big.Int) (st
533606
return f.fundHash, nil
534607
}
535608

536-
func (f *fakeWalletOperator) WithdrawUSDFC(_ context.Context, amount *big.Int) (string, error) {
609+
func (f *fakeWalletOperator) WithdrawUSDFC(ctx context.Context, amount *big.Int) (string, error) {
537610
f.withdrawAmount = new(big.Int).Set(amount)
611+
if f.onWithdraw != nil {
612+
f.onWithdraw(ctx)
613+
}
614+
if f.blockWithdraw {
615+
<-ctx.Done()
616+
return "", ctx.Err()
617+
}
538618
if f.withdrawErr != nil {
539619
return "", f.withdrawErr
540620
}
541621
return f.withdrawHash, nil
542622
}
543623

544-
func (f *fakeWalletOperator) ApproveFWSS(context.Context) (string, error) {
624+
func (f *fakeWalletOperator) ApproveFWSS(ctx context.Context) (string, error) {
545625
f.approveCalled = true
626+
if f.onApprove != nil {
627+
f.onApprove(ctx)
628+
}
629+
if f.blockApprove {
630+
<-ctx.Done()
631+
return "", ctx.Err()
632+
}
546633
if f.approveErr != nil {
547634
return "", f.approveErr
548635
}

0 commit comments

Comments
 (0)