@@ -0,0 +1,108 @@
|
||||
package hardwareprovision
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"data-terminal-cloud/internal/audit"
|
||||
"data-terminal-cloud/internal/deviceauth"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type countingRegistrar struct {
|
||||
mu sync.Mutex
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *countingRegistrar) Register(_ context.Context, _ RegisterInput) (RegisterResult, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.calls++
|
||||
return RegisterResult{DeviceSecret: "provider-device-secret-with-at-least-32-characters"}, nil
|
||||
}
|
||||
|
||||
func TestProvisionPersistsEncryptedCredentialAndReusesRegistration(t *testing.T) {
|
||||
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
t.Skip("TEST_DATABASE_URL is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := pgxpool.New(ctx, databaseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(pool.Close)
|
||||
|
||||
deviceID := uuid.NewString()
|
||||
devicePublicID := uuid.NewString()
|
||||
const tenantID = "10000000-0000-0000-0000-000000000001"
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref,activation_state,activation_reason)
|
||||
VALUES($1::uuid,$2::uuid,$3::uuid,$4,'hardware-provision-test','registered',$5,'BOUND','integration-test')`,
|
||||
deviceID, devicePublicID, tenantID, strings.Repeat("9", 64), "test://"+devicePublicID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = pool.Exec(ctx, `
|
||||
INSERT INTO device_twin(tenant_id,device_id,desired_revision,desired_config)
|
||||
VALUES($1::uuid,$2::uuid,1,'{"voice_runtime":"volc_hardware_agent"}'::jsonb)`, tenantID, deviceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, devicePublicID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM hardware_agent_provider_credential WHERE device_id=$1::uuid`, deviceID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM hardware_agent_device_binding WHERE device_id=$1::uuid`, deviceID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM device_twin WHERE device_id=$1::uuid`, deviceID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM device_activation_transition WHERE device_id=$1::uuid`, deviceID)
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM device WHERE id=$1::uuid`, deviceID)
|
||||
})
|
||||
|
||||
registrar := &countingRegistrar{}
|
||||
service, err := NewService(pool, audit.NewRepository(), Options{
|
||||
Enabled: true, InstanceID: "instance-1", ProductKey: "product-1", BotID: "bot-test", Transport: "low_load", MinimumSDKVersion: "1.1.0", ProductSecret: "unused-with-mock-registrar",
|
||||
CredentialMasterKey: strings.Repeat("ab", 32), RegisterTimeout: time.Second, DeviceNamePrefix: "dt-", Registrar: registrar,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identity := deviceauth.Identity{ID: deviceID, PublicID: devicePublicID, TenantID: tenantID, Status: "registered"}
|
||||
first, err := service.Provision(ctx, identity, "request-1", ProvisionInput{HardwareID: "hardware-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := service.Provision(ctx, identity, "request-2", ProvisionInput{HardwareID: "hardware-1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first.DeviceSecret != second.DeviceSecret || first.CredentialSource != "platform_dynamic_registration" || second.CredentialSource != "platform_stored" {
|
||||
t.Fatalf("unexpected provisioning responses: first=%+v second=%+v", first, second)
|
||||
}
|
||||
registrar.mu.Lock()
|
||||
calls := registrar.calls
|
||||
registrar.mu.Unlock()
|
||||
if calls != 1 {
|
||||
t.Fatalf("registrar calls = %d, want 1", calls)
|
||||
}
|
||||
var activationState string
|
||||
if err := pool.QueryRow(ctx, `SELECT activation_state FROM device WHERE id=$1::uuid`, deviceID).Scan(&activationState); err != nil || activationState != "ACTIVE" {
|
||||
t.Fatalf("activation state=%s err=%v, want ACTIVE", activationState, err)
|
||||
}
|
||||
var encrypted []byte
|
||||
var productKey, deviceName, credentialRef, botID, transport, minimumSDK string
|
||||
if err := pool.QueryRow(ctx, `
|
||||
SELECT pc.encrypted_secret,hb.product_key,hb.provider_device_ref,hb.credential_ref,hb.bot_id,hb.transport,hb.minimum_sdk_version
|
||||
FROM hardware_agent_provider_credential pc JOIN hardware_agent_device_binding hb ON hb.device_id=pc.device_id
|
||||
WHERE pc.device_id=$1::uuid`, deviceID).Scan(&encrypted, &productKey, &deviceName, &credentialRef, &botID, &transport, &minimumSDK); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(encrypted), first.DeviceSecret) || productKey != "product-1" || deviceName != first.DeviceName || credentialRef != first.CredentialRef || botID != "bot-test" || transport != "low_load" || minimumSDK != "1.1.0" {
|
||||
t.Fatalf("credential was not stored/bound safely")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user