109 lines
4.5 KiB
Go
109 lines
4.5 KiB
Go
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")
|
|
}
|
|
}
|