Skip to content

Commit 32e8465

Browse files
authored
chore: consolidate admin docs and focused test coverage
1 parent 4fa2c1c commit 32e8465

5 files changed

Lines changed: 152 additions & 39 deletions

File tree

docs/en/reference/admin-api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ http://127.0.0.1:9090
2424
| `/api/v1/auth/logout` | Requires a valid browser session and CSRF header; HTTP Basic auth is not accepted. |
2525
| `/api/v1/*` | Browser session cookie with CSRF for unsafe methods, or HTTP Basic auth. |
2626
| `/metrics` | Browser session cookie or HTTP Basic auth. |
27-
| `/admin/exhausted-tasks*` | Browser session cookie or HTTP Basic auth. |
27+
| `/admin/exhausted-tasks*` | Browser session cookie with CSRF for unsafe methods, or HTTP Basic auth. |
2828

2929
### Browser Sessions
3030

@@ -124,6 +124,8 @@ For object upload, the HTTP `Content-Type` is the uploaded object's content type
124124
| `GET` | `/api/v1/tasks/{id}/diagnostic` | Read task diagnostics. |
125125
| `POST` | `/api/v1/tasks/{id}/diagnostic/refresh` | Refresh diagnostics. |
126126
| `POST` | `/api/v1/tasks/{id}/retry` | Retry an exhausted task. |
127+
| `GET` | `/admin/exhausted-tasks` | List exhausted tasks. Supports `limit` up to `1000`. |
128+
| `POST` | `/admin/exhausted-tasks/{id}/retry` | Retry an exhausted task (legacy path). |
127129

128130
## Wallet and Filecoin
129131

docs/zh/reference/admin-api.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ http://127.0.0.1:9090
2424
| `/api/v1/auth/logout` | 需要有效浏览器会话和 CSRF header;不接受 HTTP Basic auth。 |
2525
| `/api/v1/*` | 浏览器 session cookie;写请求方法需要 CSRF。也可用 HTTP Basic auth。 |
2626
| `/metrics` | 浏览器 session cookie 或 HTTP Basic auth。 |
27-
| `/admin/exhausted-tasks*` | 浏览器 session cookie HTTP Basic auth。 |
27+
| `/admin/exhausted-tasks*` | 浏览器 session cookie;写请求方法需要 CSRF。也可用 HTTP Basic auth。 |
2828

2929
### 浏览器会话
3030

@@ -124,6 +124,8 @@ Admin 响应包含 `Content-Security-Policy`、`X-Content-Type-Options: nosniff`
124124
| `GET` | `/api/v1/tasks/{id}/diagnostic` | 读取任务诊断。 |
125125
| `POST` | `/api/v1/tasks/{id}/diagnostic/refresh` | 刷新诊断。 |
126126
| `POST` | `/api/v1/tasks/{id}/retry` | 重试 exhausted 任务。 |
127+
| `GET` | `/admin/exhausted-tasks` | 列出 exhausted 任务。支持最大为 `1000``limit`|
128+
| `POST` | `/admin/exhausted-tasks/{id}/retry` | 重试 exhausted 任务(遗留路径)。 |
127129

128130
## 钱包和 Filecoin
129131

internal/cache/filesystem_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,85 @@ func TestCapacityEnforcement(t *testing.T) {
412412
}
413413
}
414414

415+
func TestPutPartCapacityEnforcement(t *testing.T) {
416+
dir := t.TempDir()
417+
ctx := context.Background()
418+
419+
fs, err := NewFilesystem(dir, 10)
420+
if err != nil {
421+
t.Fatalf("NewFilesystem: %v", err)
422+
}
423+
424+
if _, err := fs.PutPart(ctx, "up-cap", 1, bytes.NewReader([]byte("12345678"))); err != nil {
425+
t.Fatalf("PutPart within capacity: %v", err)
426+
}
427+
if fs.UsedBytes() != 8 {
428+
t.Fatalf("UsedBytes after first part = %d, want 8", fs.UsedBytes())
429+
}
430+
431+
if _, err := fs.PutPart(ctx, "up-cap", 2, bytes.NewReader([]byte("12345"))); err != ErrCacheFull {
432+
t.Fatalf("PutPart over capacity err = %v, want ErrCacheFull", err)
433+
}
434+
if fs.UsedBytes() != 8 {
435+
t.Fatalf("UsedBytes after failed part = %d, want 8", fs.UsedBytes())
436+
}
437+
failedPart, err := fs.partPath("up-cap", 2)
438+
if err != nil {
439+
t.Fatalf("partPath: %v", err)
440+
}
441+
if _, err := os.Stat(failedPart); !os.IsNotExist(err) {
442+
t.Fatalf("failed part exists: %v", err)
443+
}
444+
445+
if _, err := fs.PutPart(ctx, "up-cap", 1, bytes.NewReader([]byte("abc"))); err != nil {
446+
t.Fatalf("PutPart overwrite within capacity: %v", err)
447+
}
448+
if fs.UsedBytes() != 3 {
449+
t.Fatalf("UsedBytes after smaller overwrite = %d, want 3", fs.UsedBytes())
450+
}
451+
452+
if _, err := fs.PutPart(ctx, "up-cap", 2, bytes.NewReader([]byte("12345"))); err != nil {
453+
t.Fatalf("PutPart after freeing space: %v", err)
454+
}
455+
if fs.UsedBytes() != 8 {
456+
t.Fatalf("UsedBytes after second part = %d, want 8", fs.UsedBytes())
457+
}
458+
}
459+
460+
func TestAssemblePartsCapacityEnforcement(t *testing.T) {
461+
dir := t.TempDir()
462+
ctx := context.Background()
463+
464+
fs, err := NewFilesystem(dir, 15)
465+
if err != nil {
466+
t.Fatalf("NewFilesystem: %v", err)
467+
}
468+
469+
if _, err := fs.PutPart(ctx, "up-assemble-cap", 1, bytes.NewReader([]byte("12345"))); err != nil {
470+
t.Fatalf("PutPart 1: %v", err)
471+
}
472+
if _, err := fs.PutPart(ctx, "up-assemble-cap", 2, bytes.NewReader([]byte("67890"))); err != nil {
473+
t.Fatalf("PutPart 2: %v", err)
474+
}
475+
if fs.UsedBytes() != 10 {
476+
t.Fatalf("UsedBytes after parts = %d, want 10", fs.UsedBytes())
477+
}
478+
479+
if _, _, err := fs.AssembleParts(ctx, "bkt", "key", "up-assemble-cap", []int{1, 2}); err != ErrCacheFull {
480+
t.Fatalf("AssembleParts over capacity err = %v, want ErrCacheFull", err)
481+
}
482+
if fs.UsedBytes() != 10 {
483+
t.Fatalf("UsedBytes after failed assemble = %d, want 10", fs.UsedBytes())
484+
}
485+
target, err := fs.safePath("bkt", "key")
486+
if err != nil {
487+
t.Fatalf("safePath: %v", err)
488+
}
489+
if _, err := os.Stat(target); !os.IsNotExist(err) {
490+
t.Fatalf("assembled target exists: %v", err)
491+
}
492+
}
493+
415494
func TestCapacityExactBoundary(t *testing.T) {
416495
dir := t.TempDir()
417496
ctx := context.Background()

internal/db/repository/task_repo_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,7 @@ func TestTaskRepo_CompleteByRefClearsWaitingFields(t *testing.T) {
13181318
bucket := seedBucket(t, db, "complete-ref-waiting-bucket")
13191319
waitReason := model.TaskWaitReasonDependency
13201320
statusMessage := "waiting for dependency"
1321+
lastError := "previous error"
13211322
task := &model.Task{
13221323
Type: model.TaskTypeUpload,
13231324
RefType: "bucket",
@@ -1327,6 +1328,7 @@ func TestTaskRepo_CompleteByRefClearsWaitingFields(t *testing.T) {
13271328
Status: model.TaskStatusWaiting,
13281329
WaitReason: &waitReason,
13291330
StatusMessage: &statusMessage,
1331+
LastError: &lastError,
13301332
ScheduledAt: time.Now().Add(time.Minute),
13311333
}
13321334
if err := repos.Tasks.Create(ctx, task); err != nil {

internal/worker/wallet_operation_runner_test.go

Lines changed: 65 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,43 +17,71 @@ import (
1717
)
1818

1919
func TestWalletOperationRunner_SubmitsAndConfirmsPendingOperation(t *testing.T) {
20-
db := testutil.NewTestDB(t)
21-
repos := repository.NewRepositories(db)
22-
ctx := context.Background()
23-
op, _, err := repos.WalletOperations.CreateOrGet(ctx, repository.CreateWalletOperationInput{
24-
Type: model.WalletOperationTypeFund,
25-
ClientRequestID: "fund-1",
26-
Amount: "100",
27-
})
28-
if err != nil {
29-
t.Fatalf("CreateOrGet: %v", err)
30-
}
31-
32-
txHash := common.HexToHash("0xabc")
33-
operator := &fakeWalletOperator{fundHash: txHash.Hex()}
34-
receipts := &fakeWalletReceiptChecker{receipts: map[common.Hash]*ethtypes.Receipt{
35-
txHash: {Status: ethtypes.ReceiptStatusSuccessful},
36-
}}
37-
publisher := &fakeWalletEventPublisher{}
38-
runner := NewWalletOperationRunner(repos, operator, receipts, time.Millisecond, nil, WithWalletOperationEventPublisher(publisher))
39-
40-
runner.runOnce(ctx)
41-
42-
got, err := repos.WalletOperations.GetByID(ctx, op.ID)
43-
if err != nil {
44-
t.Fatalf("GetByID: %v", err)
45-
}
46-
if got.Status != model.WalletOperationStatusConfirmed {
47-
t.Fatalf("status = %q, want confirmed", got.Status)
48-
}
49-
if got.TxHash == nil || *got.TxHash != txHash.Hex() {
50-
t.Fatalf("tx_hash = %v, want %s", got.TxHash, txHash.Hex())
51-
}
52-
if operator.fundAmount.String() != "100" {
53-
t.Fatalf("fund amount = %s, want 100", operator.fundAmount)
54-
}
55-
if !publisher.hasStatus(model.WalletOperationStatusSubmitted) || !publisher.hasStatus(model.WalletOperationStatusConfirmed) {
56-
t.Fatalf("published statuses = %v, want submitted and confirmed", publisher.statuses())
20+
tests := []struct {
21+
name string
22+
opType model.WalletOperationType
23+
amount string
24+
}{
25+
{name: "fund", opType: model.WalletOperationTypeFund, amount: "100"},
26+
{name: "withdraw", opType: model.WalletOperationTypeWithdraw, amount: "50"},
27+
}
28+
29+
for _, tt := range tests {
30+
t.Run(tt.name, func(t *testing.T) {
31+
db := testutil.NewTestDB(t)
32+
repos := repository.NewRepositories(db)
33+
ctx := context.Background()
34+
op, _, err := repos.WalletOperations.CreateOrGet(ctx, repository.CreateWalletOperationInput{
35+
Type: tt.opType,
36+
ClientRequestID: string(tt.opType) + "-1",
37+
Amount: tt.amount,
38+
})
39+
if err != nil {
40+
t.Fatalf("CreateOrGet: %v", err)
41+
}
42+
43+
txHash := common.HexToHash("0xabc")
44+
operator := &fakeWalletOperator{fundHash: txHash.Hex(), withdrawHash: txHash.Hex()}
45+
receipts := &fakeWalletReceiptChecker{receipts: map[common.Hash]*ethtypes.Receipt{
46+
txHash: {Status: ethtypes.ReceiptStatusSuccessful},
47+
}}
48+
publisher := &fakeWalletEventPublisher{}
49+
runner := NewWalletOperationRunner(repos, operator, receipts, time.Millisecond, nil, WithWalletOperationEventPublisher(publisher))
50+
51+
runner.runOnce(ctx)
52+
53+
got, err := repos.WalletOperations.GetByID(ctx, op.ID)
54+
if err != nil {
55+
t.Fatalf("GetByID: %v", err)
56+
}
57+
if got.Status != model.WalletOperationStatusConfirmed {
58+
t.Fatalf("status = %q, want confirmed", got.Status)
59+
}
60+
if got.TxHash == nil || *got.TxHash != txHash.Hex() {
61+
t.Fatalf("tx_hash = %v, want %s", got.TxHash, txHash.Hex())
62+
}
63+
switch tt.opType {
64+
case model.WalletOperationTypeFund:
65+
if operator.fundAmount == nil || operator.fundAmount.String() != tt.amount {
66+
t.Fatalf("fund amount = %v, want %s", operator.fundAmount, tt.amount)
67+
}
68+
if operator.withdrawAmount != nil {
69+
t.Fatalf("withdraw amount = %s, want no withdraw", operator.withdrawAmount)
70+
}
71+
case model.WalletOperationTypeWithdraw:
72+
if operator.withdrawAmount == nil || operator.withdrawAmount.String() != tt.amount {
73+
t.Fatalf("withdraw amount = %v, want %s", operator.withdrawAmount, tt.amount)
74+
}
75+
if operator.fundAmount != nil {
76+
t.Fatalf("fund amount = %s, want no fund", operator.fundAmount)
77+
}
78+
default:
79+
t.Fatalf("unexpected operation type %q", tt.opType)
80+
}
81+
if !publisher.hasStatus(model.WalletOperationStatusSubmitted) || !publisher.hasStatus(model.WalletOperationStatusConfirmed) {
82+
t.Fatalf("published statuses = %v, want submitted and confirmed", publisher.statuses())
83+
}
84+
})
5785
}
5886
}
5987

0 commit comments

Comments
 (0)