feat(t217): verify authorized order dry runs

This commit is contained in:
QiuSW
2026-07-28 16:52:29 +08:00
parent 913107c28d
commit 1e87b6273a
45 changed files with 4358 additions and 128 deletions
@@ -713,6 +713,9 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
if err != nil {
t.Fatalf("migration.New() after review error = %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("order dry-run migration down: %v", err)
}
if err := runner.Down(context.Background()); err != nil {
t.Fatalf("device command migration down: %v", err)
}
@@ -721,8 +724,8 @@ func TestDeviceExecutionResultsAreIdempotentAndAuditable(t *testing.T) {
}
if applied, err := runner.Up(context.Background()); err != nil {
t.Fatalf("restore device command migration: %v", err)
} else if applied != 1 {
t.Fatalf("restored migrations = %d, want 1", applied)
} else if applied != 2 {
t.Fatalf("restored migrations = %d, want 2", applied)
}
completePayload := fmt.Sprintf(
@@ -885,6 +888,7 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
SchemaVersion int `json:"schema_version"`
TaskID string `json:"task_id"`
ExecutionID string `json:"execution_id"`
OriginalSKU string `json:"original_sku"`
Quantity int `json:"quantity"`
CommandSHA256 string `json:"command_sha256"`
AuthorizationStatus string `json:"authorization_status"`
@@ -905,6 +909,7 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
command.SchemaVersion != 1 ||
command.TaskID != taskID ||
command.ExecutionID != started.Execution.ID ||
command.OriginalSKU == "" ||
command.Quantity != 2 ||
len(command.CommandSHA256) != 64 ||
command.AuthorizationStatus != "DELIVERED" ||
@@ -997,10 +1002,149 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
) {
t.Fatalf("acknowledged pull = %s", acknowledgedPull.Body.String())
}
var deliveredEvents, acknowledgedEvents int
startDryRunPayload := fmt.Sprintf(
`{"device_id":%q,"execution_id":%q,"claim_generation":%d,"command_id":%q,"command_sha256":%q}`,
deviceTestDeviceID,
started.Execution.ID,
started.Task.ClaimGeneration,
command.ID,
command.CommandSHA256,
)
startDryRun := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID + "/order-dry-runs/start",
contentType: "application/json",
body: strings.NewReader(startDryRunPayload),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
idempotencyKey: "order-dry-run-start",
})
requireDeviceStatus(t, startDryRun, http.StatusOK)
if !strings.Contains(startDryRun.Body.String(), `"status":"PREPARING"`) {
t.Fatalf("dry-run start = %s", startDryRun.Body.String())
}
startDryRunReplay := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID + "/order-dry-runs/start",
contentType: "application/json",
body: strings.NewReader(startDryRunPayload),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
idempotencyKey: "order-dry-run-start",
})
requireDeviceStatus(t, startDryRunReplay, http.StatusOK)
if !strings.Contains(startDryRunReplay.Body.String(), `"replayed":true`) {
t.Fatalf("dry-run start replay = %s", startDryRunReplay.Body.String())
}
executingPull := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID + "/commands/next",
contentType: "application/json",
body: strings.NewReader(pullPayload),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
})
requireDeviceStatus(t, executingPull, http.StatusOK)
if !strings.Contains(
executingPull.Body.String(),
`"authorization_status":"EXECUTING"`,
) {
t.Fatalf("executing pull = %s", executingPull.Body.String())
}
const dryRunEvidenceID = "00000000-0000-4000-8000-000000000077"
dryRunEvidenceSHA := strings.Repeat("e", 64)
if _, err := fixture.db.Exec(
`INSERT INTO execution_evidence_assets (
id, task_id, execution_id, media_type, size_bytes, sha256,
storage_key, created_at, received_after_execution_expiry
) VALUES (?, ?, ?, 'image/jpeg', 10, ?, ?, ?, 0)`,
dryRunEvidenceID,
taskID,
started.Execution.ID,
dryRunEvidenceSHA,
"dry-run/order-confirmation.jpg",
time.Now().UTC().Format(time.RFC3339Nano),
); err != nil {
t.Fatalf("seed dry-run evidence: %v", err)
}
readyDryRunPayload := fmt.Sprintf(
`{"device_id":%q,"execution_id":%q,"claim_generation":%d,"command_sha256":%q,"card_signature":%q,"detail_signature":%q,"observed_title":%q,"selected_sku":%q,"quantity":2,"unit_price_cents":2150,"total_price_cents":4300,"evidence_asset_id":%q,"evidence_sha256":%q}`,
deviceTestDeviceID,
started.Execution.ID,
started.Task.ClaimGeneration,
command.CommandSHA256,
command.Candidate.CardSignature,
strings.Repeat("f", 64),
command.Candidate.Title,
command.OriginalSKU,
dryRunEvidenceID,
dryRunEvidenceSHA,
)
badSKUDryRun := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID +
"/order-dry-runs/" + command.ID + "/ready",
contentType: "application/json",
body: strings.NewReader(strings.Replace(
readyDryRunPayload,
fmt.Sprintf(`"selected_sku":%q`, command.OriginalSKU),
`"selected_sku":"wrong-sku"`,
1,
)),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
idempotencyKey: "order-dry-run-ready-bad-sku",
})
requireDeviceStatus(t, badSKUDryRun, http.StatusConflict)
badTotalDryRun := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID +
"/order-dry-runs/" + command.ID + "/ready",
contentType: "application/json",
body: strings.NewReader(strings.Replace(
readyDryRunPayload,
`"total_price_cents":4300`,
`"total_price_cents":4301`,
1,
)),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
idempotencyKey: "order-dry-run-ready-bad-total",
})
requireDeviceStatus(t, badTotalDryRun, http.StatusConflict)
readyDryRun := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID + "/order-dry-runs/" + command.ID + "/ready",
contentType: "application/json",
body: strings.NewReader(readyDryRunPayload),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
idempotencyKey: "order-dry-run-ready",
})
requireDeviceStatus(t, readyDryRun, http.StatusOK)
if !strings.Contains(readyDryRun.Body.String(), `"status":"READY"`) {
t.Fatalf("dry-run ready = %s", readyDryRun.Body.String())
}
readyDryRunReplay := performDeviceRequest(t, fixture.router, deviceRequest{
method: http.MethodPost,
target: "/api/v1/tasks/" + taskID +
"/order-dry-runs/" + command.ID + "/ready",
contentType: "application/json",
body: strings.NewReader(readyDryRunPayload),
bearerToken: testOpaqueToken,
claimToken: testOpaqueToken,
idempotencyKey: "order-dry-run-ready",
})
requireDeviceStatus(t, readyDryRunReplay, http.StatusOK)
if !strings.Contains(readyDryRunReplay.Body.String(), `"replayed":true`) {
t.Fatalf("dry-run ready replay = %s", readyDryRunReplay.Body.String())
}
var deliveredEvents, acknowledgedEvents, dryRunStartedEvents, dryRunReadyEvents int
for eventType, target := range map[string]*int{
"ORDER_AUTHORIZATION_DELIVERED": &deliveredEvents,
"ORDER_AUTHORIZATION_ACKNOWLEDGED": &acknowledgedEvents,
"ORDER_DRY_RUN_STARTED": &dryRunStartedEvents,
"ORDER_DRY_RUN_READY": &dryRunReadyEvents,
} {
if err := fixture.db.QueryRow(
`SELECT COUNT(*) FROM task_events
@@ -1011,11 +1155,14 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
t.Fatalf("count %s events: %v", eventType, err)
}
}
if deliveredEvents != 1 || acknowledgedEvents != 1 {
if deliveredEvents != 1 || acknowledgedEvents != 1 ||
dryRunStartedEvents != 1 || dryRunReadyEvents != 1 {
t.Fatalf(
"delivery/ack events = %d/%d",
"delivery/ack/dry-run events = %d/%d/%d/%d",
deliveredEvents,
acknowledgedEvents,
dryRunStartedEvents,
dryRunReadyEvents,
)
}
runner, err := migration.New(fixture.db)
@@ -1023,7 +1170,7 @@ func TestDeviceOrderCommandDeliveryAndAcknowledgementAreRecoverable(
t.Fatalf("migration.New() error = %v", err)
}
if err := runner.Down(context.Background()); err == nil {
t.Fatal("device command migration down succeeded with command data")
t.Fatal("order dry-run migration down succeeded with dry-run data")
}
}
@@ -1352,12 +1499,17 @@ func newDeviceHTTPFixture(t *testing.T) *deviceHTTPFixture {
if err != nil {
t.Fatalf("usecase.NewDeviceOrderCommandService() error = %v", err)
}
dryRuns, err := usecase.NewOrderDryRunService(store, clock, ids)
if err != nil {
t.Fatalf("usecase.NewOrderDryRunService() error = %v", err)
}
deviceRoutes, err := NewDeviceRouteRegistrar(
DeviceServices{
Lifecycle: lifecycle,
Assets: assets,
Results: results,
Commands: commands,
DryRuns: dryRuns,
},
)
if err != nil {