136 lines
7.0 KiB
Go
136 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|