Skip to content

Commit ea6de2d

Browse files
authored
chore: consolidate tests, docs, and bucket UI
1 parent 0c6f1b1 commit ea6de2d

7 files changed

Lines changed: 265 additions & 98 deletions

File tree

docs/en/reference/cli-api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export SYNAPS3_ADMIN_PASSWORD='replace-with-admin-password'
5252
synaps3 admin status
5353
synaps3 admin s3-user create
5454
synaps3 admin s3-user list
55+
synaps3 admin s3-user update <access-key> --role userplus
56+
synaps3 admin s3-user rotate-secret <access-key>
5557
synaps3 admin settings get
5658
synaps3 admin settings set cache.max_size_gb=200
5759
synaps3 admin task stats
@@ -70,6 +72,7 @@ High-risk changes require confirmation:
7072
```bash
7173
synaps3 admin settings set filecoin.network=mainnet --yes
7274
synaps3 admin s3-user create --role admin --yes
75+
synaps3 admin s3-user update <access-key> --role admin --yes
7376
synaps3 admin s3-user delete <access-key> --yes
7477
```
7578

docs/zh/reference/cli-api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export SYNAPS3_ADMIN_PASSWORD='replace-with-admin-password'
5252
synaps3 admin status
5353
synaps3 admin s3-user create
5454
synaps3 admin s3-user list
55+
synaps3 admin s3-user update <access-key> --role userplus
56+
synaps3 admin s3-user rotate-secret <access-key>
5557
synaps3 admin settings get
5658
synaps3 admin settings set cache.max_size_gb=200
5759
synaps3 admin task stats
@@ -70,6 +72,7 @@ Admin API 包含设置、钱包操作、任务重试和 S3 用户管理的写端
7072
```bash
7173
synaps3 admin settings set filecoin.network=mainnet --yes
7274
synaps3 admin s3-user create --role admin --yes
75+
synaps3 admin s3-user update <access-key> --role admin --yes
7376
synaps3 admin s3-user delete <access-key> --yes
7477
```
7578

internal/admin/api_observability_test.go

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"github.com/strahe/synaps3/internal/testutil"
1616
)
1717

18-
func TestAPIObservabilityRefreshRunsThroughAuthenticatedAdminSurface(t *testing.T) {
18+
func TestAPIObservabilityRefreshRunsThroughAdminHandler(t *testing.T) {
1919
var calls int32
2020
service := observability.NewService(observability.ServiceOptions{
2121
Checker: &observabilityAPIRefreshChecker{
@@ -150,20 +150,98 @@ func TestAPIObservabilityDataSetBucketFilters(t *testing.T) {
150150
}
151151
}
152152

153+
func TestAPIObservabilityProviders(t *testing.T) {
154+
service := observability.NewService(observability.ServiceOptions{
155+
Store: &observabilityAPIStore{
156+
providers: []observability.ProviderState{
157+
{ProviderID: onChainID(t, "101"), Status: observability.StatusAvailable},
158+
},
159+
},
160+
})
161+
srv := &Server{addr: "127.0.0.1:9090", observability: service, logger: testLogger()}
162+
req := httptest.NewRequest(http.MethodGet, "/api/v1/observability/providers", nil)
163+
rr := httptest.NewRecorder()
164+
165+
srv.handleAPIObservabilityProviders(rr, req)
166+
167+
if rr.Code != http.StatusOK {
168+
t.Fatalf("status = %d, want %d, body=%s", rr.Code, http.StatusOK, rr.Body.String())
169+
}
170+
171+
var body observability.ProviderObservationPage
172+
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
173+
t.Fatalf("Decode: %v", err)
174+
}
175+
if len(body.Items) != 1 || body.Items[0].Facts.ProviderID.String() != "101" {
176+
t.Fatalf("items = %+v, want provider 101", body.Items)
177+
}
178+
}
179+
180+
func TestAPIObservabilityRefreshDataSets(t *testing.T) {
181+
var calls int32
182+
service := observability.NewService(observability.ServiceOptions{
183+
Checker: &observabilityAPIRefreshChecker{
184+
dataSets: func(context.Context, time.Time, []observability.LocalDataSet) ([]observability.DataSetState, error) {
185+
atomic.AddInt32(&calls, 1)
186+
return []observability.DataSetState{
187+
{
188+
LocalDataSetID: 101,
189+
BucketID: 7,
190+
BucketName: "photos",
191+
ProviderID: onChainID(t, "202"),
192+
LocalStatus: "ready",
193+
Status: observability.StatusAvailable,
194+
},
195+
}, nil
196+
},
197+
},
198+
LocalDataSets: observability.LocalDataSetSourceFunc(func(context.Context) ([]observability.LocalDataSet, error) { return nil, nil }),
199+
Store: &observabilityAPIStore{},
200+
})
201+
srv := &Server{addr: "127.0.0.1:9090", observability: service, logger: testLogger()}
202+
req := httptest.NewRequest(http.MethodPost, "/api/v1/observability/data-sets/refresh", nil)
203+
rr := httptest.NewRecorder()
204+
205+
srv.handleAPIRefreshObservabilityDataSets(rr, req)
206+
207+
if rr.Code != http.StatusOK {
208+
t.Fatalf("status = %d, want %d, body=%s", rr.Code, http.StatusOK, rr.Body.String())
209+
}
210+
if gotCalls := atomic.LoadInt32(&calls); gotCalls != 1 {
211+
t.Fatalf("refresh calls = %d, want 1", gotCalls)
212+
}
213+
214+
var body observability.DataSetObservationPage
215+
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
216+
t.Fatalf("Decode: %v", err)
217+
}
218+
if len(body.Items) != 1 || body.Items[0].Facts.LocalDataSetID != 101 {
219+
t.Fatalf("items = %+v, want dataset 101", body.Items)
220+
}
221+
}
222+
153223
type observabilityAPIRefreshChecker struct {
154224
providers func(context.Context, time.Time, []observability.LocalDataSet) ([]observability.ProviderState, error)
225+
dataSets func(context.Context, time.Time, []observability.LocalDataSet) ([]observability.DataSetState, error)
155226
}
156227

157228
func (c *observabilityAPIRefreshChecker) CheckProviders(ctx context.Context, checkedAt time.Time, local []observability.LocalDataSet) ([]observability.ProviderState, error) {
229+
if c.providers == nil {
230+
return nil, nil
231+
}
158232
return c.providers(ctx, checkedAt, local)
159233
}
160234

161-
func (c *observabilityAPIRefreshChecker) CheckDataSets(context.Context, time.Time, []observability.LocalDataSet) ([]observability.DataSetState, error) {
235+
func (c *observabilityAPIRefreshChecker) CheckDataSets(ctx context.Context, checkedAt time.Time, local []observability.LocalDataSet) ([]observability.DataSetState, error) {
236+
if c.dataSets != nil {
237+
return c.dataSets(ctx, checkedAt, local)
238+
}
162239
return nil, nil
163240
}
164241

165242
type observabilityAPIStore struct {
166243
providers []observability.ProviderState
244+
dataSets []observability.DataSetState
167245
providerLastCheckedAt *time.Time
168246
dataSetLastCheckedAt *time.Time
169247
lastDataSetListOptions observability.ListOptions
@@ -186,14 +264,22 @@ func (s *observabilityAPIStore) ListProviderStates(_ context.Context, opts obser
186264
}, nil
187265
}
188266

189-
func (s *observabilityAPIStore) ReplaceDataSetStates(_ context.Context, checkedAt time.Time, _ []observability.DataSetState) error {
267+
func (s *observabilityAPIStore) ReplaceDataSetStates(_ context.Context, checkedAt time.Time, states []observability.DataSetState) error {
268+
s.dataSets = states
190269
s.dataSetLastCheckedAt = &checkedAt
191270
return nil
192271
}
193272

194273
func (s *observabilityAPIStore) ListDataSetStates(_ context.Context, opts observability.ListOptions) (observability.DataSetStatePage, error) {
195274
s.lastDataSetListOptions = opts
196-
return observability.DataSetStatePage{LastCheckedAt: s.dataSetLastCheckedAt, Limit: opts.Limit, Offset: opts.Offset}, nil
275+
return observability.DataSetStatePage{
276+
Items: s.dataSets,
277+
Summary: observability.Summary{Total: len(s.dataSets), Available: len(s.dataSets)},
278+
LastCheckedAt: s.dataSetLastCheckedAt,
279+
Total: len(s.dataSets),
280+
Limit: opts.Limit,
281+
Offset: opts.Offset,
282+
}, nil
197283
}
198284

199285
func (s *observabilityAPIStore) GetDataSetStatesByLocalIDs(context.Context, []int64) (map[int64]observability.DataSetState, error) {

internal/admin/api_tasks_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,66 @@ func TestAPIRetryExhaustedHTTPStatuses(t *testing.T) {
127127
}
128128
}
129129

130+
func TestAPITaskStats(t *testing.T) {
131+
db := testutil.NewTestDB(t)
132+
repos := repository.NewRepositories(db)
133+
ctx := context.Background()
134+
135+
tasks := []*model.Task{
136+
{
137+
Type: model.TaskTypeUpload,
138+
RefType: "object",
139+
RefID: 1,
140+
RefVersionID: "01J000000000000000STAT01",
141+
IdempotencyKey: "api-task-stats-queued-1",
142+
Status: model.TaskStatusQueued,
143+
},
144+
{
145+
Type: model.TaskTypeUpload,
146+
RefType: "object",
147+
RefID: 2,
148+
RefVersionID: "01J000000000000000STAT02",
149+
IdempotencyKey: "api-task-stats-queued-2",
150+
Status: model.TaskStatusQueued,
151+
},
152+
{
153+
Type: model.TaskTypeUpload,
154+
RefType: "object",
155+
RefID: 3,
156+
RefVersionID: "01J000000000000000STAT03",
157+
IdempotencyKey: "api-task-stats-running",
158+
Status: model.TaskStatusRunning,
159+
},
160+
}
161+
for _, task := range tasks {
162+
if err := repos.Tasks.Create(ctx, task); err != nil {
163+
t.Fatalf("Create task %q: %v", task.IdempotencyKey, err)
164+
}
165+
}
166+
167+
srv := New(":0", db, nil, 0, repos, nil, nil, config.DefaultFilecoinCopies, testLogger())
168+
rr := httptest.NewRecorder()
169+
srv.handleAPITaskStats(rr, httptest.NewRequest(http.MethodGet, "/api/v1/tasks/stats", nil))
170+
if rr.Code != http.StatusOK {
171+
t.Fatalf("status = %d, want 200; body=%s", rr.Code, rr.Body.String())
172+
}
173+
174+
var body []repository.TaskStatusCount
175+
if err := json.NewDecoder(rr.Body).Decode(&body); err != nil {
176+
t.Fatalf("Decode stats: %v", err)
177+
}
178+
counts := make(map[[2]string]int64, len(body))
179+
for _, count := range body {
180+
counts[[2]string{count.Type, count.Status}] = count.Count
181+
}
182+
if got := counts[[2]string{string(model.TaskTypeUpload), string(model.TaskStatusQueued)}]; got != 2 {
183+
t.Fatalf("queued upload count = %d, want 2", got)
184+
}
185+
if got := counts[[2]string{string(model.TaskTypeUpload), string(model.TaskStatusRunning)}]; got != 1 {
186+
t.Fatalf("running upload count = %d, want 1", got)
187+
}
188+
}
189+
130190
func TestAPITasksStageFilter(t *testing.T) {
131191
db := testutil.NewTestDB(t)
132192
repos := repository.NewRepositories(db)

internal/db/repository/task_repo_test.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ func seedTask(t *testing.T, repos *repository.Repositories, taskType model.TaskT
2626
return task
2727
}
2828

29+
func assertTaskIDs(t *testing.T, tasks []model.Task, want ...int64) {
30+
t.Helper()
31+
if len(tasks) != len(want) {
32+
t.Fatalf("task count = %d, want %d; tasks=%#v", len(tasks), len(want), tasks)
33+
}
34+
for i, task := range tasks {
35+
if task.ID != want[i] {
36+
t.Fatalf("task[%d].ID = %d, want %d; tasks=%#v", i, task.ID, want[i], tasks)
37+
}
38+
}
39+
}
40+
2941
func TestTaskRepo_ClaimReady(t *testing.T) {
3042
db := testDB(t)
3143
repos := repository.NewRepositories(db)
@@ -470,7 +482,7 @@ func TestTaskRepo_Complete_NotRunning(t *testing.T) {
470482
}
471483
}
472484

473-
func TestTaskRepo_Fail(t *testing.T) {
485+
func TestTaskRepo_FailRunning(t *testing.T) {
474486
db := testDB(t)
475487
repos := repository.NewRepositories(db)
476488
ctx := context.Background()
@@ -495,6 +507,21 @@ func TestTaskRepo_Fail(t *testing.T) {
495507
if task.CompletedAt == nil {
496508
t.Error("expected completed_at to be set")
497509
}
510+
if task.ClaimedAt != nil || task.LeaseUntil != nil || task.StartedAt != nil {
511+
t.Fatalf("failed task lease fields = claimed:%v lease:%v started:%v, want cleared", task.ClaimedAt, task.LeaseUntil, task.StartedAt)
512+
}
513+
514+
queued := seedTask(t, repos, model.TaskTypeUpload)
515+
now := time.Now()
516+
leaseUntil := now.Add(5 * time.Minute)
517+
mustExec(t, db, `UPDATE tasks SET claimed_at = ?, lease_until = ?, started_at = ? WHERE id = ?`, now, leaseUntil, now, queued.ID)
518+
queued.ClaimedAt = &now
519+
queued.LeaseUntil = &leaseUntil
520+
queued.StartedAt = &now
521+
err := repos.Tasks.FailRunning(ctx, queued, "should fail")
522+
if err == nil {
523+
t.Fatal("expected error failing queued task")
524+
}
498525
}
499526

500527
func TestTaskRepo_ReleaseExpiredLeases(t *testing.T) {
@@ -856,15 +883,20 @@ func TestTaskRepo_List(t *testing.T) {
856883
}
857884

858885
// Seed tasks: 2 upload (queued), 1 evict_cache (queued).
859-
seedTask(t, repos, model.TaskTypeUpload)
860-
seedTask(t, repos, model.TaskTypeUpload)
861-
seedTask(t, repos, model.TaskTypeEvictCache)
886+
firstUpload := seedTask(t, repos, model.TaskTypeUpload)
887+
secondUpload := seedTask(t, repos, model.TaskTypeUpload)
888+
evict := seedTask(t, repos, model.TaskTypeEvictCache)
862889

863890
// Claim one upload task to make it running.
864891
claimed, _ := repos.Tasks.ClaimReady(ctx, model.TaskTypeUpload, 5*time.Minute)
865892
if claimed == nil {
866893
t.Fatal("setup: could not claim task")
867894
}
895+
runningUploadID := claimed.ID
896+
queuedUploadID := firstUpload.ID
897+
if runningUploadID == firstUpload.ID {
898+
queuedUploadID = secondUpload.ID
899+
}
868900

869901
// List all — should return 3.
870902
tasks, total, err = repos.Tasks.List(ctx, "", "", "", 10, 0)
@@ -877,24 +909,27 @@ func TestTaskRepo_List(t *testing.T) {
877909
if len(tasks) != 3 {
878910
t.Errorf("expected 3 tasks, got %d", len(tasks))
879911
}
912+
assertTaskIDs(t, tasks, evict.ID, secondUpload.ID, firstUpload.ID)
880913

881914
// Filter by type.
882-
_, total, err = repos.Tasks.List(ctx, string(model.TaskTypeUpload), "", "", 10, 0)
915+
tasks, total, err = repos.Tasks.List(ctx, string(model.TaskTypeUpload), "", "", 10, 0)
883916
if err != nil {
884917
t.Fatalf("List by type: %v", err)
885918
}
886919
if total != 2 {
887920
t.Errorf("expected 2 upload, got %d", total)
888921
}
922+
assertTaskIDs(t, tasks, secondUpload.ID, firstUpload.ID)
889923

890924
// Filter by status.
891-
_, total, err = repos.Tasks.List(ctx, "", "", string(model.TaskStatusQueued), 10, 0)
925+
tasks, total, err = repos.Tasks.List(ctx, "", "", string(model.TaskStatusQueued), 10, 0)
892926
if err != nil {
893927
t.Fatalf("List by status: %v", err)
894928
}
895929
if total != 2 {
896930
t.Errorf("expected 2 queued, got %d", total)
897931
}
932+
assertTaskIDs(t, tasks, evict.ID, queuedUploadID)
898933

899934
// Filter by type + status.
900935
tasks, total, err = repos.Tasks.List(ctx, string(model.TaskTypeUpload), "", string(model.TaskStatusRunning), 10, 0)
@@ -907,6 +942,7 @@ func TestTaskRepo_List(t *testing.T) {
907942
if len(tasks) != 1 {
908943
t.Errorf("expected 1 task, got %d", len(tasks))
909944
}
945+
assertTaskIDs(t, tasks, runningUploadID)
910946

911947
// Pagination: limit 2, offset 0.
912948
tasks, total, err = repos.Tasks.List(ctx, "", "", "", 2, 0)
@@ -919,6 +955,7 @@ func TestTaskRepo_List(t *testing.T) {
919955
if len(tasks) != 2 {
920956
t.Errorf("expected 2 tasks with limit=2, got %d", len(tasks))
921957
}
958+
assertTaskIDs(t, tasks, evict.ID, secondUpload.ID)
922959

923960
// Pagination: limit 2, offset 2 — should return 1.
924961
tasks, total, err = repos.Tasks.List(ctx, "", "", "", 2, 2)
@@ -931,6 +968,7 @@ func TestTaskRepo_List(t *testing.T) {
931968
if len(tasks) != 1 {
932969
t.Errorf("expected 1 task at offset 2, got %d", len(tasks))
933970
}
971+
assertTaskIDs(t, tasks, firstUpload.ID)
934972
}
935973

936974
func TestTaskRepo_ListFiltersByStage(t *testing.T) {

0 commit comments

Comments
 (0)