Files

136 lines
7.0 KiB
Go
Raw Permalink Normal View History

2026-08-26 06:41:07 +08:00
package device
import (
"context"
"encoding/json"
"errors"
"os"
"testing"
"time"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"github.com/jackc/pgx/v5/pgxpool"
)
const (
telemetryDeviceID = "94000000-0000-0000-0000-000000000501"
telemetryDevicePublicID = "94000000-0000-0000-0000-000000000511"
telemetryCommandID = "94000000-0000-0000-0000-000000000601"
telemetryCommandPublic = "94000000-0000-0000-0000-000000000611"
)
func TestTelemetryServiceAtomicallyUpdatesTwinAndCommand(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := database.Open(ctx, databaseURL, "device-telemetry-test")
if err != nil {
t.Fatal(err)
}
defer pool.Close()
cleanupTelemetryFixture(t, ctx, pool)
defer cleanupTelemetryFixture(t, context.Background(), pool)
if _, err := pool.Exec(ctx, `INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref)
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',repeat('7',64),'telemetry-test','registered','test://telemetry')`, telemetryDeviceID, telemetryDevicePublicID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO device_twin(device_id,tenant_id,desired_revision,reported_revision)
VALUES($1::uuid,'10000000-0000-0000-0000-000000000001',2,1)`, telemetryDeviceID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO device_command(id,public_id,tenant_id,device_id,command_type,payload,status,requested_by,idempotency_key,request_digest,expires_at,delivery_attempts)
VALUES($1::uuid,$2::uuid,'10000000-0000-0000-0000-000000000001',$3::uuid,'diagnostics','{}','DELIVERED','10000000-0000-0000-0000-000000000101','telemetry-fixture',repeat('8',64),now()+interval '1 hour',1)`, telemetryCommandID, telemetryCommandPublic, telemetryDeviceID); err != nil {
t.Fatal(err)
}
service, err := NewTelemetryService(pool, idempotency.NewExecutor(pool))
if err != nil {
t.Fatal(err)
}
identity := deviceauth.Identity{ID: telemetryDeviceID, PublicID: telemetryDevicePublicID, TenantID: "10000000-0000-0000-0000-000000000001", Status: "registered"}
rssi := -51
input := TelemetryBatchInput{Events: []TelemetryEvent{
{EventKey: "status-1", SequenceNo: 1, OccurredAt: time.Date(2026, 8, 23, 12, 0, 0, 0, time.UTC), EventType: "status", Status: &TelemetryStatus{
FirmwareVersion: "1.2.3", ReportedConfigRevision: 1, Network: TelemetryNetwork{Type: "wifi", RSSI: &rssi},
RTC: TelemetryRTC{}, RecordingQueue: TelemetryRecordingQueue{PendingSegments: 2},
}},
{EventKey: "error-2", SequenceNo: 2, OccurredAt: time.Date(2026, 8, 23, 12, 0, 1, 0, time.UTC), EventType: "error", Error: &TelemetryError{Code: "RTC_RECONNECTING", Severity: "warning"}},
{EventKey: "command-3", SequenceNo: 3, OccurredAt: time.Date(2026, 8, 23, 12, 0, 2, 0, time.UTC), EventType: "command_result", CommandResult: &TelemetryCommandResult{
CommandID: telemetryCommandPublic, Status: "SUCCEEDED", ResultSummary: map[string]any{"result_code": "OK"},
}},
}}
body, _ := json.Marshal(input)
response, replayed, err := service.Submit(ctx, identity, "telemetry-idempotency-0001", body, input)
if err != nil || replayed || response.AcceptedCount != 3 || response.DuplicateCount != 0 || len(response.Actions) != 1 || response.Actions[0].Type != "refresh_config" {
t.Fatalf("response=%#v replayed=%v err=%v", response, replayed, err)
}
replayedResponse, replayed, err := service.Submit(ctx, identity, "telemetry-idempotency-0001", body, input)
if err != nil || !replayed || replayedResponse.AcceptedCount != 3 {
t.Fatalf("replayed response=%#v replayed=%v err=%v", replayedResponse, replayed, err)
}
duplicateResponse, replayed, err := service.Submit(ctx, identity, "telemetry-idempotency-0002", body, input)
if err != nil || replayed || duplicateResponse.AcceptedCount != 0 || duplicateResponse.DuplicateCount != 3 {
t.Fatalf("duplicate response=%#v replayed=%v err=%v", duplicateResponse, replayed, err)
}
conflicting := input
conflicting.Events = append([]TelemetryEvent(nil), input.Events...)
conflicting.Events[1].Error = &TelemetryError{Code: "RTC_FAILED", Severity: "error"}
conflictingBody, _ := json.Marshal(conflicting)
if _, _, err := service.Submit(ctx, identity, "telemetry-idempotency-0003", conflictingBody, conflicting); err == nil {
t.Fatal("conflicting telemetry was accepted")
} else {
var conflict *TelemetryConflictError
if !errors.As(err, &conflict) || len(conflict.Conflicts) != 1 {
t.Fatalf("conflicting telemetry error=%v", err)
}
}
ahead := TelemetryBatchInput{Events: []TelemetryEvent{{EventKey: "status-4", SequenceNo: 4, OccurredAt: time.Now().UTC(), EventType: "status", Status: &TelemetryStatus{
FirmwareVersion: "1.2.3", ReportedConfigRevision: 3, Network: TelemetryNetwork{Type: "wifi"}, RTC: TelemetryRTC{}, RecordingQueue: TelemetryRecordingQueue{},
}}}}
aheadBody, _ := json.Marshal(ahead)
if _, _, err := service.Submit(ctx, identity, "telemetry-idempotency-0004", aheadBody, ahead); !errors.Is(err, ErrTelemetryConfigAhead) {
t.Fatalf("ahead configuration error=%v", err)
}
var telemetrySequence int64
var firmware, errorCode, errorSeverity, commandStatus string
var events int
if err := pool.QueryRow(ctx, `SELECT telemetry_sequence,firmware_version,last_error_code,last_error_severity FROM device_twin WHERE device_id=$1::uuid`, telemetryDeviceID).Scan(&telemetrySequence, &firmware, &errorCode, &errorSeverity); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT status FROM device_command WHERE id=$1::uuid`, telemetryCommandID).Scan(&commandStatus); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM device_telemetry_event WHERE device_id=$1::uuid`, telemetryDeviceID).Scan(&events); err != nil {
t.Fatal(err)
}
if telemetrySequence != 3 || firmware != "1.2.3" || errorCode != "RTC_RECONNECTING" || errorSeverity != "warning" || commandStatus != "SUCCEEDED" || events != 3 {
t.Fatalf("sequence=%d firmware=%s error=%s/%s command=%s events=%d", telemetrySequence, firmware, errorCode, errorSeverity, commandStatus, events)
}
}
func cleanupTelemetryFixture(t *testing.T, ctx context.Context, pool *pgxpool.Pool) {
t.Helper()
statements := []struct {
query string
arg string
}{
{`DELETE FROM device_telemetry_event WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM idempotency_record WHERE actor_key=$1`, "device:" + telemetryDevicePublicID},
{`DELETE FROM device_presence_state WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM device_command WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM device_twin WHERE device_id=$1::uuid`, telemetryDeviceID},
{`DELETE FROM device WHERE id=$1::uuid`, telemetryDeviceID},
}
for _, statement := range statements {
if _, err := pool.Exec(ctx, statement.query, statement.arg); err != nil {
t.Fatalf("cleanup telemetry fixture: %v", err)
}
}
}