xworkmate-bridge/internal/acp/gateway_identity_test.go

195 lines
6.5 KiB
Go

package acp
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"xworkmate-bridge/internal/gatewayruntime"
)
func TestBridgeGatewayIdentityUsesOpenClawDeviceFingerprint(t *testing.T) {
identityPath := filepath.Join(t.TempDir(), "openclaw-device.json")
t.Setenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH", identityPath)
resetBridgeGatewayIdentityForTest()
t.Cleanup(resetBridgeGatewayIdentityForTest)
identity := newBridgeGatewayIdentity()
if strings.HasPrefix(identity.DeviceID, "xworkmate-bridge-") {
t.Fatalf("device id must not use legacy bridge prefix: %q", identity.DeviceID)
}
publicKey, err := base64.RawURLEncoding.DecodeString(identity.PublicKeyBase64URL)
if err != nil {
t.Fatalf("decode public key: %v", err)
}
sum := sha256.Sum256(publicKey)
if got, want := identity.DeviceID, hex.EncodeToString(sum[:]); got != want {
t.Fatalf("device id = %q, want public-key fingerprint %q", got, want)
}
info, err := os.Stat(identityPath)
if err != nil {
t.Fatalf("identity file was not created: %v", err)
}
if got := info.Mode().Perm(); got != 0o600 {
t.Fatalf("identity file mode = %o, want 600", got)
}
resetBridgeGatewayIdentityForTest()
reloaded := newBridgeGatewayIdentity()
if reloaded.DeviceID != identity.DeviceID {
t.Fatalf("identity should be stable across reloads: got %q want %q", reloaded.DeviceID, identity.DeviceID)
}
}
func TestBridgeGatewayIdentityCorrectsMismatchedStoredDeviceID(t *testing.T) {
identityPath := filepath.Join(t.TempDir(), "openclaw-device.json")
t.Setenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH", identityPath)
resetBridgeGatewayIdentityForTest()
t.Cleanup(resetBridgeGatewayIdentityForTest)
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
if err := os.MkdirAll(filepath.Dir(identityPath), 0o700); err != nil {
t.Fatalf("create identity dir: %v", err)
}
writeStoredIdentity(t, identityPath, storedBridgeGatewayIdentity{
Version: 1,
DeviceID: "legacy-wrong-device-id",
PublicKeyBase64URL: base64.RawURLEncoding.EncodeToString(publicKey),
PrivateKeyBase64URL: base64.RawURLEncoding.EncodeToString(privateKey),
CreatedAtMs: 1,
DeviceToken: "device-token-1",
})
identity, token := bridgeGatewayOpenClawCredentials()
sum := sha256.Sum256(publicKey)
if got, want := identity.DeviceID, hex.EncodeToString(sum[:]); got != want {
t.Fatalf("device id = %q, want corrected fingerprint %q", got, want)
}
if token != "device-token-1" {
t.Fatalf("expected stored device token to survive correction, got %q", token)
}
var stored storedBridgeGatewayIdentity
readStoredIdentity(t, identityPath, &stored)
if stored.DeviceID != identity.DeviceID {
t.Fatalf("stored device id was not corrected: %#v", stored)
}
}
func TestBridgeGatewayIdentityRegeneratesInvalidStoredKeyPair(t *testing.T) {
identityPath := filepath.Join(t.TempDir(), "openclaw-device.json")
t.Setenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH", identityPath)
resetBridgeGatewayIdentityForTest()
t.Cleanup(resetBridgeGatewayIdentityForTest)
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate public key: %v", err)
}
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate private key: %v", err)
}
if err := os.MkdirAll(filepath.Dir(identityPath), 0o700); err != nil {
t.Fatalf("create identity dir: %v", err)
}
writeStoredIdentity(t, identityPath, storedBridgeGatewayIdentity{
Version: 1,
DeviceID: "bad",
PublicKeyBase64URL: base64.RawURLEncoding.EncodeToString(publicKey),
PrivateKeyBase64URL: base64.RawURLEncoding.EncodeToString(privateKey),
CreatedAtMs: 1,
DeviceToken: "stale-token",
})
identity, token := bridgeGatewayOpenClawCredentials()
if identity.DeviceID == "bad" || token != "" {
t.Fatalf("expected invalid identity to regenerate and drop token, got identity=%#v token=%q", identity, token)
}
publicKeyBytes, err := base64.RawURLEncoding.DecodeString(identity.PublicKeyBase64URL)
if err != nil {
t.Fatalf("decode regenerated public key: %v", err)
}
sum := sha256.Sum256(publicKeyBytes)
if identity.DeviceID != hex.EncodeToString(sum[:]) {
t.Fatalf("regenerated device id does not match public key")
}
}
func TestBridgeGatewayIdentityPersistsReturnedDeviceToken(t *testing.T) {
identityPath := filepath.Join(t.TempDir(), "openclaw-device.json")
t.Setenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH", identityPath)
resetBridgeGatewayIdentityForTest()
t.Cleanup(resetBridgeGatewayIdentityForTest)
identity := newBridgeGatewayIdentity()
saveBridgeGatewayDeviceToken("device-token-2")
resetBridgeGatewayIdentityForTest()
reloaded, token := bridgeGatewayOpenClawCredentials()
if reloaded.DeviceID != identity.DeviceID {
t.Fatalf("reloaded identity = %q, want %q", reloaded.DeviceID, identity.DeviceID)
}
if token != "device-token-2" {
t.Fatalf("device token = %q, want device-token-2", token)
}
}
func TestBridgeGatewayIdentityClearsStoredDeviceToken(t *testing.T) {
identityPath := filepath.Join(t.TempDir(), "openclaw-device.json")
t.Setenv("XWORKMATE_BRIDGE_OPENCLAW_IDENTITY_PATH", identityPath)
resetBridgeGatewayIdentityForTest()
t.Cleanup(resetBridgeGatewayIdentityForTest)
identity := newBridgeGatewayIdentity()
saveBridgeGatewayDeviceToken("device-token-2")
clearBridgeGatewayDeviceToken()
resetBridgeGatewayIdentityForTest()
reloaded, token := bridgeGatewayOpenClawCredentials()
if reloaded.DeviceID != identity.DeviceID {
t.Fatalf("reloaded identity = %q, want %q", reloaded.DeviceID, identity.DeviceID)
}
if token != "" {
t.Fatalf("device token should be cleared, got %q", token)
}
}
func resetBridgeGatewayIdentityForTest() {
bridgeGatewayIdentity.Lock()
defer bridgeGatewayIdentity.Unlock()
bridgeGatewayIdentity.value = gatewayruntime.DeviceIdentity{}
bridgeGatewayIdentity.deviceToken = ""
}
func writeStoredIdentity(t *testing.T, path string, stored storedBridgeGatewayIdentity) {
t.Helper()
data, err := json.Marshal(stored)
if err != nil {
t.Fatalf("marshal identity: %v", err)
}
if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil {
t.Fatalf("write identity: %v", err)
}
}
func readStoredIdentity(t *testing.T, path string, target *storedBridgeGatewayIdentity) {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read identity: %v", err)
}
if err := json.Unmarshal(data, target); err != nil {
t.Fatalf("unmarshal identity: %v", err)
}
}