Files
MyGoNavi/internal/app/connection_package_transfer_test.go
Syngnat c7cf9526de 🐛 fix(security): 修复 macOS 无法打开应用及三平台依赖系统钥匙串的问题
- 密文存储:新增 dailysecret 本地存储引擎,连接/代理/AI 密钥不再依赖系统钥匙串
- 启动迁移:自动将已有钥匙串密文迁移到本地 JSON,用户无感知
- WebKit 迁移:从旧版 Wails WebKit LocalStorage 中恢复连接与代理数据
- DMG 修复:移除 --sandbox-safe 避免扩展属性污染签名,新增 xattr 清理与签名校验
- 安全适配:钥匙串不可用时标记完成而非回滚,消除无钥匙串环境下的阻塞
- 出口脱敏:所有连接/代理 API 返回前统一 sanitize 防止密文泄漏
2026-04-13 12:40:25 +08:00

1023 lines
29 KiB
Go

package app
import (
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/secretstore"
)
func TestBuildConnectionPackagePayloadIncludesSecretBundles(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "conn-1",
Name: "Primary",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
Password: "db-secret",
UseSSH: true,
SSH: connection.SSHConfig{
Host: "jump.local",
Port: 22,
User: "ops",
Password: "ssh-secret",
},
URI: "postgres://postgres:db-secret@db.local/app",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
payload, err := app.buildConnectionPackagePayload()
if err != nil {
t.Fatalf("buildConnectionPackagePayload returned error: %v", err)
}
if _, parseErr := time.Parse(time.RFC3339, payload.ExportedAt); parseErr != nil {
t.Fatalf("expected RFC3339 exportedAt, got %q", payload.ExportedAt)
}
if len(payload.Connections) != 1 {
t.Fatalf("expected 1 connection in payload, got %d", len(payload.Connections))
}
item := payload.Connections[0]
if item.ID != "conn-1" {
t.Fatalf("expected ID=conn-1, got %q", item.ID)
}
if item.Config.Password != "" {
t.Fatalf("payload metadata must stay secretless, got password=%q", item.Config.Password)
}
if item.Config.SSH.Password != "" {
t.Fatalf("payload metadata must stay secretless for SSH, got %q", item.Config.SSH.Password)
}
if item.Config.URI != "" {
t.Fatalf("payload metadata must stay secretless for URI, got %q", item.Config.URI)
}
if item.Secrets.Password != "db-secret" {
t.Fatalf("expected bundled primary password, got %q", item.Secrets.Password)
}
if item.Secrets.SSHPassword != "ssh-secret" {
t.Fatalf("expected bundled SSH password, got %q", item.Secrets.SSHPassword)
}
if item.Secrets.OpaqueURI != "postgres://postgres:db-secret@db.local/app" {
t.Fatalf("expected bundled URI secret, got %q", item.Secrets.OpaqueURI)
}
raw, err := json.Marshal(payload)
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
if strings.Contains(string(raw), "secretRef") {
t.Fatalf("payload must not contain secretRef, got %s", string(raw))
}
}
func TestBuildExportedConnectionPackageWithoutSecretsUsesV2AppManagedAndImportsWithoutPasswords(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "conn-v2-no-secrets",
Name: "Primary",
Config: connection.ConnectionConfig{
ID: "conn-v2-no-secrets",
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
Password: "db-secret",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
raw, err := app.buildExportedConnectionPackage(ConnectionExportOptions{
IncludeSecrets: false,
FilePassword: "ignored-password",
})
if err != nil {
t.Fatalf("buildExportedConnectionPackage returned error: %v", err)
}
var file connectionPackageFileV2
if err := json.Unmarshal(raw, &file); err != nil {
t.Fatalf("json.Unmarshal returned error: %v", err)
}
if file.V != connectionPackageSchemaVersionV2 {
t.Fatalf("expected v2 package, got v=%d", file.V)
}
if file.P != connectionPackageProtectionAppManaged {
t.Fatalf("expected app-managed protection, got p=%d", file.P)
}
if strings.Contains(string(raw), `"secrets"`) {
t.Fatalf("expected exported JSON to omit secrets when IncludeSecrets=false, got %s", string(raw))
}
importApp := NewAppWithSecretStore(newFakeAppSecretStore())
importApp.configDir = t.TempDir()
imported, err := importApp.ImportConnectionsPayload(string(raw), "")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected 1 imported connection, got %d", len(imported))
}
if imported[0].HasPrimaryPassword {
t.Fatal("expected imported connection to keep empty password when secrets are excluded")
}
resolved, err := importApp.resolveConnectionSecrets(imported[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "" {
t.Fatalf("expected imported password to be empty, got %q", resolved.Password)
}
}
func TestImportConnectionPackagePayloadOverwritesExistingSecrets(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "conn-1",
Name: "Primary",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.old.local",
Port: 5432,
User: "postgres",
Password: "old-primary",
UseSSH: true,
SSH: connection.SSHConfig{
Host: "jump.old.local",
Port: 22,
User: "ops",
Password: "old-ssh",
},
URI: "postgres://old",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
imported, err := app.importConnectionPackagePayload(connectionPackagePayload{
Connections: []connectionPackageItem{
{
ID: "conn-1",
Name: "Imported",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.new.local",
Port: 5432,
User: "postgres",
UseSSH: true,
SSH: connection.SSHConfig{
Host: "jump.new.local",
Port: 22,
User: "ops",
},
},
Secrets: connectionSecretBundle{
Password: "new-primary",
},
},
},
})
if err != nil {
t.Fatalf("importConnectionPackagePayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected 1 imported item, got %d", len(imported))
}
if imported[0].Name != "Imported" {
t.Fatalf("expected imported name, got %q", imported[0].Name)
}
if !imported[0].HasPrimaryPassword {
t.Fatal("expected primary password to be present after overwrite")
}
if imported[0].HasSSHPassword {
t.Fatal("expected SSH password to be cleared by package overwrite")
}
if imported[0].HasOpaqueURI {
t.Fatal("expected URI secret to be cleared by package overwrite")
}
resolved, err := app.resolveConnectionSecrets(imported[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "new-primary" {
t.Fatalf("expected primary password to be overwritten, got %q", resolved.Password)
}
if resolved.SSH.Password != "" {
t.Fatalf("expected SSH password to be cleared, got %q", resolved.SSH.Password)
}
if resolved.URI != "" {
t.Fatalf("expected URI secret to be cleared, got %q", resolved.URI)
}
}
func TestImportConnectionPackagePayloadLatestEntryWinsForSameID(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
imported, err := app.importConnectionPackagePayload(connectionPackagePayload{
Connections: []connectionPackageItem{
{
ID: "conn-dup",
Name: "First",
Config: connection.ConnectionConfig{
ID: "conn-dup",
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
},
Secrets: connectionSecretBundle{Password: "first-secret"},
},
{
ID: "conn-dup",
Name: "Second",
Config: connection.ConnectionConfig{
ID: "conn-dup",
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
},
Secrets: connectionSecretBundle{Password: "second-secret"},
},
},
})
if err != nil {
t.Fatalf("importConnectionPackagePayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected duplicate ids to return 1 final imported item, got %d", len(imported))
}
if imported[0].Name != "Second" {
t.Fatalf("expected returned import result to keep latest entry, got %q", imported[0].Name)
}
saved, err := app.GetSavedConnections()
if err != nil {
t.Fatalf("GetSavedConnections returned error: %v", err)
}
if len(saved) != 1 {
t.Fatalf("expected 1 saved item after duplicate id overwrite, got %d", len(saved))
}
if saved[0].Name != "Second" {
t.Fatalf("expected latest item to win, got %q", saved[0].Name)
}
resolved, err := app.resolveConnectionSecrets(saved[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "second-secret" {
t.Fatalf("expected latest secret to win, got %q", resolved.Password)
}
}
func TestImportConnectionsPayloadLegacyJSONRollsBackOnSaveFailure(t *testing.T) {
withTestGOOS(t, "linux")
failRef, err := secretstore.BuildRef(savedConnectionSecretKind, "legacy-2")
if err != nil {
t.Fatalf("BuildRef returned error: %v", err)
}
store := newFailOnPutSecretStore(failRef)
app := NewAppWithSecretStore(store)
app.configDir = t.TempDir()
_, err = app.SaveConnection(connection.SavedConnectionInput{
ID: "legacy-1",
Name: "Existing Legacy",
Config: connection.ConnectionConfig{
ID: "legacy-1",
Type: "postgres",
Host: "db.old.local",
Port: 5432,
User: "postgres",
Password: "old-primary",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
raw, err := json.Marshal([]connection.LegacySavedConnection{
{
ID: "legacy-1",
Name: "Imported Existing Legacy",
Config: connection.ConnectionConfig{
ID: "legacy-1",
Type: "postgres",
Host: "db.new.local",
Port: 5432,
User: "postgres",
},
},
{
ID: "legacy-2",
Name: "Imported New Legacy",
Config: connection.ConnectionConfig{
ID: "legacy-2",
Type: "mysql",
Host: "db.second.local",
Port: 3306,
User: "root",
Password: "second-primary",
},
},
})
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
imported, err := app.ImportConnectionsPayload(string(raw), "ignored")
if err != nil {
t.Fatalf("expected ImportConnectionsPayload to succeed without secret store, got %v", err)
}
if len(imported) != 2 {
t.Fatalf("expected 2 imported results, got %#v", imported)
}
saved, err := app.GetSavedConnections()
if err != nil {
t.Fatalf("GetSavedConnections returned error: %v", err)
}
if len(saved) != 2 {
t.Fatalf("expected import to keep 2 legacy connections, got %d", len(saved))
}
if saved[0].ID != "legacy-1" || saved[0].Name != "Imported Existing Legacy" {
t.Fatalf("expected updated legacy metadata, got %#v", saved[0])
}
if saved[0].Config.Host != "db.new.local" {
t.Fatalf("expected import to update legacy host, got %q", saved[0].Config.Host)
}
resolved, err := app.resolveConnectionSecrets(saved[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "" {
t.Fatalf("expected legacy import without password to clear stored password, got %q", resolved.Password)
}
if _, err := store.Get(failRef); !os.IsNotExist(err) {
t.Fatalf("expected rollback to remove partially imported legacy secret ref, got err=%v", err)
}
}
func TestImportLegacyConnectionsRollbackRemovesGeneratedSecretRefs(t *testing.T) {
withTestGOOS(t, "linux")
failRef, err := secretstore.BuildRef(savedConnectionSecretKind, "legacy-2")
if err != nil {
t.Fatalf("BuildRef returned error: %v", err)
}
store := newFailOnPutSecretStore(failRef)
app := NewAppWithSecretStore(store)
app.configDir = t.TempDir()
imported, err := app.ImportLegacyConnections([]connection.LegacySavedConnection{
{
Name: "Generated ID Legacy",
Config: connection.ConnectionConfig{
Type: "postgres",
Host: "db.generated.local",
Port: 5432,
User: "postgres",
Password: "generated-secret",
},
},
{
ID: "legacy-2",
Name: "Will Fail",
Config: connection.ConnectionConfig{
ID: "legacy-2",
Type: "mysql",
Host: "db.fail.local",
Port: 3306,
User: "root",
Password: "fail-secret",
},
},
})
if err != nil {
t.Fatalf("expected ImportLegacyConnections to succeed without secret store, got %v", err)
}
if len(imported) != 2 {
t.Fatalf("expected 2 imported results after import, got %#v", imported)
}
saved, err := app.GetSavedConnections()
if err != nil {
t.Fatalf("GetSavedConnections returned error: %v", err)
}
if len(saved) != 2 {
t.Fatalf("expected imported connections to be persisted, got %d saved connections", len(saved))
}
if got := len(store.base.items); got != 0 {
t.Fatalf("expected rollback to remove generated secret refs, got %d remaining items", got)
}
if _, err := store.Get(failRef); !os.IsNotExist(err) {
t.Fatalf("expected rollback to remove failed explicit secret ref, got err=%v", err)
}
}
func TestImportConnectionPackagePayloadRollsBackOnSaveFailure(t *testing.T) {
withTestGOOS(t, "linux")
failRef, err := secretstore.BuildRef(savedConnectionSecretKind, "conn-2")
if err != nil {
t.Fatalf("BuildRef returned error: %v", err)
}
store := newFailOnPutSecretStore(failRef)
app := NewAppWithSecretStore(store)
app.configDir = t.TempDir()
_, err = app.SaveConnection(connection.SavedConnectionInput{
ID: "conn-1",
Name: "Existing",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.old.local",
Port: 5432,
User: "postgres",
Password: "old-primary",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
imported, err := app.importConnectionPackagePayload(connectionPackagePayload{
Connections: []connectionPackageItem{
{
ID: "conn-1",
Name: "Imported Existing",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.new.local",
Port: 5432,
User: "postgres",
},
Secrets: connectionSecretBundle{Password: "new-primary"},
},
{
ID: "conn-2",
Name: "Imported New",
Config: connection.ConnectionConfig{
ID: "conn-2",
Type: "mysql",
Host: "db.second.local",
Port: 3306,
User: "root",
},
Secrets: connectionSecretBundle{Password: "second-primary"},
},
},
})
if err != nil {
t.Fatalf("expected importConnectionPackagePayload to succeed without secret store, got %v", err)
}
if len(imported) != 2 {
t.Fatalf("expected 2 imported results after import, got %#v", imported)
}
saved, err := app.GetSavedConnections()
if err != nil {
t.Fatalf("GetSavedConnections returned error: %v", err)
}
if len(saved) != 2 {
t.Fatalf("expected import to keep 2 connections, got %d", len(saved))
}
if saved[0].ID != "conn-1" || saved[0].Name != "Imported Existing" {
t.Fatalf("expected imported connection metadata, got %#v", saved[0])
}
if saved[0].Config.Host != "db.new.local" {
t.Fatalf("expected import to update host, got %q", saved[0].Config.Host)
}
resolved, err := app.resolveConnectionSecrets(saved[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "new-primary" {
t.Fatalf("expected import to update primary password, got %q", resolved.Password)
}
if _, err := store.Get(failRef); !os.IsNotExist(err) {
t.Fatalf("expected rollback to remove partially imported secret ref, got err=%v", err)
}
}
func TestImportConnectionsPayloadLegacyJSONClearsExistingSecretWhenMissing(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "legacy-1",
Name: "Legacy",
Config: connection.ConnectionConfig{
ID: "legacy-1",
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
Password: "legacy-secret",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
raw, err := json.Marshal([]connection.LegacySavedConnection{
{
ID: "legacy-1",
Name: "Legacy Updated",
Config: connection.ConnectionConfig{
ID: "legacy-1",
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
},
},
})
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
imported, err := app.ImportConnectionsPayload(string(raw), "ignored")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected 1 imported item, got %d", len(imported))
}
if imported[0].Name != "Legacy Updated" {
t.Fatalf("expected legacy metadata to be overwritten, got %q", imported[0].Name)
}
resolved, err := app.resolveConnectionSecrets(imported[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "" {
t.Fatalf("expected legacy import to clear existing secret when the imported file omits it, got %q", resolved.Password)
}
}
func TestImportConnectionsPayloadLegacyJSONLatestEntryWinsForSameID(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
raw, err := json.Marshal([]connection.LegacySavedConnection{
{
ID: "legacy-dup",
Name: "First",
Config: connection.ConnectionConfig{
ID: "legacy-dup",
Type: "postgres",
Host: "db.first.local",
Port: 5432,
User: "postgres",
Password: "first-secret",
},
},
{
ID: "legacy-dup",
Name: "Second",
Config: connection.ConnectionConfig{
ID: "legacy-dup",
Type: "postgres",
Host: "db.second.local",
Port: 5432,
User: "postgres",
Password: "second-secret",
},
},
})
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
imported, err := app.ImportConnectionsPayload(string(raw), "ignored")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected duplicate legacy ids to return 1 final imported item, got %d", len(imported))
}
if imported[0].Name != "Second" {
t.Fatalf("expected returned import result to keep latest legacy entry, got %q", imported[0].Name)
}
saved, err := app.GetSavedConnections()
if err != nil {
t.Fatalf("GetSavedConnections returned error: %v", err)
}
if len(saved) != 1 {
t.Fatalf("expected 1 saved legacy item after duplicate id overwrite, got %d", len(saved))
}
if saved[0].Name != "Second" {
t.Fatalf("expected latest legacy item to win, got %q", saved[0].Name)
}
resolved, err := app.resolveConnectionSecrets(saved[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "second-secret" {
t.Fatalf("expected latest legacy secret to win, got %q", resolved.Password)
}
}
func TestImportConnectionsPayloadLegacyJSONLatestEntryWithoutPasswordDoesNotKeepEarlierDuplicateSecret(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
raw, err := json.Marshal([]connection.LegacySavedConnection{
{
ID: "legacy-dup",
Name: "First",
Config: connection.ConnectionConfig{
ID: "legacy-dup",
Type: "postgres",
Host: "db.first.local",
Port: 5432,
User: "postgres",
Password: "first-secret",
},
},
{
ID: "legacy-dup",
Name: "Second",
Config: connection.ConnectionConfig{
ID: "legacy-dup",
Type: "postgres",
Host: "db.second.local",
Port: 5432,
User: "postgres",
},
},
})
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
imported, err := app.ImportConnectionsPayload(string(raw), "ignored")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected duplicate legacy ids to return 1 final imported item, got %d", len(imported))
}
saved, err := app.GetSavedConnections()
if err != nil {
t.Fatalf("GetSavedConnections returned error: %v", err)
}
if len(saved) != 1 {
t.Fatalf("expected 1 saved legacy item after duplicate id overwrite, got %d", len(saved))
}
if saved[0].HasPrimaryPassword {
t.Fatalf("expected latest legacy item without password to clear earlier duplicate secret, got view=%#v", saved[0])
}
resolved, err := app.resolveConnectionSecrets(saved[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "" {
t.Fatalf("expected latest legacy item without password to keep empty secret, got %q", resolved.Password)
}
}
func TestImportConnectionsPayloadEnvelopeRejectsOversizedPayloadWithDedicatedError(t *testing.T) {
raw, err := json.Marshal(connectionPackageFile{
SchemaVersion: connectionPackageSchemaVersion,
Kind: connectionPackageKind,
Cipher: connectionPackageCipher,
KDF: connectionPackageKDFSpec{
Name: connectionPackageKDFName,
MemoryKiB: connectionPackageKDFDefaultMemoryKiB,
TimeCost: connectionPackageKDFDefaultTimeCost,
Parallelism: connectionPackageKDFDefaultParallelism,
Salt: "AAAAAAAAAAAAAAAAAAAAAA==",
},
Nonce: "AAAAAAAAAAAAAAAA",
Payload: strings.Repeat("A", connectionPackageMaxPayloadBase64Bytes+4),
})
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err = app.ImportConnectionsPayload(string(raw), "package-password")
if !errors.Is(err, errConnectionPackagePayloadTooLarge) {
t.Fatalf("expected errConnectionPackagePayloadTooLarge, got %v", err)
}
}
func TestImportConnectionsPayloadEnvelopeRequiresPassword(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
raw := `{
"schemaVersion": 1,
"kind": "gonavi_connection_package",
"cipher": "AES-256-GCM",
"kdf": {
"name": "Argon2id",
"memoryKiB": 65536,
"timeCost": 3,
"parallelism": 4,
"salt": "salt"
},
"nonce": "nonce",
"payload": "payload"
}`
_, err := app.ImportConnectionsPayload(raw, "")
if !errors.Is(err, errConnectionPackagePasswordRequired) {
t.Fatalf("expected errConnectionPackagePasswordRequired, got %v", err)
}
}
func TestImportConnectionsPayloadEnvelopeImportsAndOverwritesSecrets(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "conn-1",
Name: "Existing",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.old.local",
Port: 5432,
User: "postgres",
Password: "old-primary",
UseSSH: true,
SSH: connection.SSHConfig{
Host: "jump.old.local",
Port: 22,
User: "ops",
Password: "old-ssh",
},
URI: "postgres://old",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
file, err := encryptConnectionPackage(connectionPackagePayload{
Connections: []connectionPackageItem{
{
ID: "conn-1",
Name: "Imported",
Config: connection.ConnectionConfig{
ID: "conn-1",
Type: "postgres",
Host: "db.new.local",
Port: 5432,
User: "postgres",
},
Secrets: connectionSecretBundle{
Password: "new-primary",
},
},
},
}, "package-password")
if err != nil {
t.Fatalf("encryptConnectionPackage returned error: %v", err)
}
raw, err := json.Marshal(file)
if err != nil {
t.Fatalf("json.Marshal returned error: %v", err)
}
imported, err := app.ImportConnectionsPayload(string(raw), "package-password")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected 1 imported item, got %d", len(imported))
}
if imported[0].Name != "Imported" {
t.Fatalf("expected imported name, got %q", imported[0].Name)
}
if !imported[0].HasPrimaryPassword {
t.Fatal("expected primary password after envelope import")
}
if imported[0].HasSSHPassword {
t.Fatal("expected missing SSH password in package to clear old secret")
}
if imported[0].HasOpaqueURI {
t.Fatal("expected missing URI in package to clear old secret")
}
resolved, err := app.resolveConnectionSecrets(imported[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "new-primary" {
t.Fatalf("expected primary password to be overwritten, got %q", resolved.Password)
}
if resolved.SSH.Password != "" {
t.Fatalf("expected SSH password to be cleared, got %q", resolved.SSH.Password)
}
if resolved.URI != "" {
t.Fatalf("expected URI secret to be cleared, got %q", resolved.URI)
}
}
func TestBuildExportedConnectionPackageWithSecretsUsesV2AppManagedEncryption(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
saveConnectionForPackageExport(t, app, "conn-v2-app", "app-secret")
raw, err := app.buildExportedConnectionPackage(ConnectionExportOptions{
IncludeSecrets: true,
})
if err != nil {
t.Fatalf("buildExportedConnectionPackage returned error: %v", err)
}
rawString := string(raw)
if !isConnectionPackageV2AppManaged(rawString) {
t.Fatalf("expected app-managed export, got %s", rawString)
}
for _, forbidden := range []string{
"app-secret",
"schemaVersion",
"cipher",
"ENC:",
} {
if strings.Contains(rawString, forbidden) {
t.Fatalf("v2 p=1 export must not contain %q: %s", forbidden, rawString)
}
}
imported, err := app.ImportConnectionsPayload(rawString, "")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected 1 imported item, got %d", len(imported))
}
resolved, err := app.resolveConnectionSecrets(imported[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "app-secret" {
t.Fatalf("expected v2 p=1 import to restore password, got %q", resolved.Password)
}
}
func TestBuildExportedConnectionPackageWithFilePasswordUsesV2ProtectedEnvelope(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
saveConnectionForPackageExport(t, app, "conn-v2-protected", "protected-secret")
raw, err := app.buildExportedConnectionPackage(ConnectionExportOptions{
IncludeSecrets: true,
FilePassword: "package-password",
})
if err != nil {
t.Fatalf("buildExportedConnectionPackage returned error: %v", err)
}
rawString := string(raw)
if !isConnectionPackageV2Protected(rawString) {
t.Fatalf("expected password-protected export, got %s", rawString)
}
if strings.Contains(rawString, "protected-secret") {
t.Fatalf("v2 p=2 export must not contain plaintext secret: %s", rawString)
}
_, err = app.ImportConnectionsPayload(rawString, "wrong-password")
if !errors.Is(err, errConnectionPackageDecryptFailed) {
t.Fatalf("wrong v2 p=2 password should return unified error, got %v", err)
}
imported, err := app.ImportConnectionsPayload(rawString, "package-password")
if err != nil {
t.Fatalf("ImportConnectionsPayload returned error: %v", err)
}
if len(imported) != 1 {
t.Fatalf("expected 1 imported item, got %d", len(imported))
}
resolved, err := app.resolveConnectionSecrets(imported[0].Config)
if err != nil {
t.Fatalf("resolveConnectionSecrets returned error: %v", err)
}
if resolved.Password != "protected-secret" {
t.Fatalf("expected v2 p=2 import to restore password, got %q", resolved.Password)
}
}
func TestNormalizeConnectionPackageExportFilenameAddsExtension(t *testing.T) {
filename := normalizeConnectionPackageExportFilename(`C:\tmp\connections`)
if !strings.HasSuffix(filename, connectionPackageExtension) {
t.Fatalf("expected filename to end with %q, got %q", connectionPackageExtension, filename)
}
alreadyExtended := normalizeConnectionPackageExportFilename(`C:\tmp\connections` + connectionPackageExtension)
if alreadyExtended != `C:\tmp\connections`+connectionPackageExtension {
t.Fatalf("expected existing extension to be preserved, got %q", alreadyExtended)
}
}
type failOnPutSecretStore struct {
base *fakeAppSecretStore
failRef string
}
func newFailOnPutSecretStore(failRef string) *failOnPutSecretStore {
return &failOnPutSecretStore{
base: newFakeAppSecretStore(),
failRef: failRef,
}
}
func saveConnectionForPackageExport(t *testing.T, app *App, id string, primaryPassword string) {
t.Helper()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: id,
Name: "Exported " + id,
Config: connection.ConnectionConfig{
ID: id,
Type: "postgres",
Host: "db.local",
Port: 5432,
User: "postgres",
Password: primaryPassword,
UseSSH: true,
SSH: connection.SSHConfig{
Host: "jump.local",
Port: 22,
User: "ops",
Password: "ssh-" + primaryPassword,
},
URI: "postgres://postgres:" + primaryPassword + "@db.local/app",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
}
func (s *failOnPutSecretStore) Put(ref string, payload []byte) error {
if ref == s.failRef {
return errors.New("injected put failure")
}
return s.base.Put(ref, payload)
}
func (s *failOnPutSecretStore) Get(ref string) ([]byte, error) {
return s.base.Get(ref)
}
func (s *failOnPutSecretStore) Delete(ref string) error {
return s.base.Delete(ref)
}
func (s *failOnPutSecretStore) HealthCheck() error {
return s.base.HealthCheck()
}