Files
plainleaf/cli/config_test.go
T
Zef Hemel b5bdab5d1a CLI work
* Replaced initial node.js prototype of CLI with proper Go
  implementation
* Added code signing for CLI + server binaries for macOS
2026-03-29 16:48:39 +02:00

288 lines
7.3 KiB
Go

package cli
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncryptDecryptRoundTrip(t *testing.T) {
secrets := []string{
"simple-token",
"p@ssw0rd!with$pecial&chars",
"",
"a",
"unicode: こんにちは世界",
}
for _, secret := range secrets {
encrypted, err := Encrypt(secret)
require.NoError(t, err)
assert.NotEqual(t, secret, encrypted)
decrypted, err := Decrypt(encrypted)
require.NoError(t, err)
assert.Equal(t, secret, decrypted)
}
}
func TestEncryptProducesDifferentCiphertexts(t *testing.T) {
e1, err := Encrypt("same-input")
require.NoError(t, err)
e2, err := Encrypt("same-input")
require.NoError(t, err)
assert.NotEqual(t, e1, e2, "different IVs should produce different ciphertexts")
}
func TestDecryptInvalidFormat(t *testing.T) {
cases := []string{
"not-valid",
"only:two",
"",
}
for _, input := range cases {
_, err := Decrypt(input)
assert.Error(t, err)
}
}
func TestDecryptCorruptedData(t *testing.T) {
encrypted, err := Encrypt("test")
require.NoError(t, err)
// Corrupt the ciphertext portion
_, err = Decrypt(encrypted + "AAAA")
assert.Error(t, err)
}
func TestConfigSaveLoad(t *testing.T) {
tmpDir := t.TempDir()
cfgPath := filepath.Join(tmpDir, "config.json")
cfg := &Config{
Spaces: []SpaceConfig{
{
ID: "test-id-1",
Name: "work",
URL: "https://work.example.com",
Auth: AuthConfig{
Method: "token",
},
},
{
ID: "test-id-2",
Name: "personal",
URL: "https://personal.example.com",
Auth: AuthConfig{
Method: "none",
},
},
},
}
// Use SaveConfig's marshaling logic manually (since SaveConfig writes to a fixed path)
rawSpaces := make([]json.RawMessage, 0, len(cfg.Spaces))
for _, s := range cfg.Spaces {
r, err := marshalSpace(s)
require.NoError(t, err)
rawSpaces = append(rawSpaces, r)
}
wrapper := struct {
Spaces []json.RawMessage `json:"spaces"`
}{Spaces: rawSpaces}
data, err := json.MarshalIndent(wrapper, "", " ")
require.NoError(t, err)
data = append(data, '\n')
require.NoError(t, os.WriteFile(cfgPath, data, 0600))
// Read and parse
readData, err := os.ReadFile(cfgPath)
require.NoError(t, err)
loaded, err := parseConfig(readData)
require.NoError(t, err)
assert.Len(t, loaded.Spaces, 2)
assert.Equal(t, "test-id-1", loaded.Spaces[0].ID)
assert.Equal(t, "work", loaded.Spaces[0].Name)
assert.Equal(t, "https://work.example.com", loaded.Spaces[0].URL)
assert.Equal(t, "token", loaded.Spaces[0].Auth.Method)
assert.Equal(t, "test-id-2", loaded.Spaces[1].ID)
assert.Equal(t, "personal", loaded.Spaces[1].Name)
}
func TestPassthroughPreservesUnknownFields(t *testing.T) {
// Simulate a config file with App-specific fields that Core doesn't model
input := `{
"spaces": [
{
"id": "abc-123",
"name": "my-notes",
"folderPath": "/home/user/notes",
"preferredPort": 3010,
"auth": { "method": "none" },
"sync": { "enabled": true, "remoteUrl": "https://remote.example.com" },
"lastOpened": 1711526400000,
"windows": [{ "pagePath": "index", "x": 100, "y": 200, "width": 1200, "height": 800 }],
"customAppField": "should-survive"
},
{
"id": "def-456",
"name": "remote-wiki",
"url": "https://wiki.example.com",
"auth": { "method": "token", "encryptedToken": "enc-tok" }
}
]
}`
// Parse
cfg, err := parseConfig([]byte(input))
require.NoError(t, err)
require.Len(t, cfg.Spaces, 2)
// Verify Core fields are read correctly
assert.Equal(t, "abc-123", cfg.Spaces[0].ID)
assert.Equal(t, "my-notes", cfg.Spaces[0].Name)
assert.Equal(t, "/home/user/notes", cfg.Spaces[0].FolderPath)
assert.Equal(t, "none", cfg.Spaces[0].Auth.Method)
assert.Equal(t, "def-456", cfg.Spaces[1].ID)
assert.Equal(t, "https://wiki.example.com", cfg.Spaces[1].URL)
assert.Equal(t, "token", cfg.Spaces[1].Auth.Method)
assert.Equal(t, "enc-tok", cfg.Spaces[1].Auth.EncryptedToken)
// Modify a Core field
cfg.Spaces[0].Name = "renamed-notes"
// Marshal back
rawSpaces := make([]json.RawMessage, 0, len(cfg.Spaces))
for _, s := range cfg.Spaces {
r, err := marshalSpace(s)
require.NoError(t, err)
rawSpaces = append(rawSpaces, r)
}
wrapper := struct {
Spaces []json.RawMessage `json:"spaces"`
}{Spaces: rawSpaces}
output, err := json.MarshalIndent(wrapper, "", " ")
require.NoError(t, err)
// Re-parse the output as generic JSON to check unknown fields survived
var generic struct {
Spaces []map[string]json.RawMessage `json:"spaces"`
}
require.NoError(t, json.Unmarshal(output, &generic))
require.Len(t, generic.Spaces, 2)
space0 := generic.Spaces[0]
// Core field was updated
var name string
require.NoError(t, json.Unmarshal(space0["name"], &name))
assert.Equal(t, "renamed-notes", name)
// App-specific fields survived
assert.Contains(t, space0, "preferredPort")
var port int
require.NoError(t, json.Unmarshal(space0["preferredPort"], &port))
assert.Equal(t, 3010, port)
assert.Contains(t, space0, "lastOpened")
assert.Contains(t, space0, "windows")
assert.Contains(t, space0, "sync")
assert.Contains(t, space0, "customAppField")
var customField string
require.NoError(t, json.Unmarshal(space0["customAppField"], &customField))
assert.Equal(t, "should-survive", customField)
}
func TestNewSpaceHasNoRaw(t *testing.T) {
// A new space created by CLI has no Raw — should marshal cleanly
s := SpaceConfig{
ID: "new-id",
Name: "new-space",
URL: "https://example.com",
Auth: AuthConfig{Method: "none"},
}
data, err := marshalSpace(s)
require.NoError(t, err)
var m map[string]any
require.NoError(t, json.Unmarshal(data, &m))
assert.Equal(t, "new-id", m["id"])
assert.Equal(t, "new-space", m["name"])
assert.Equal(t, "https://example.com", m["url"])
}
func TestNewUUID(t *testing.T) {
id1 := NewUUID()
id2 := NewUUID()
assert.NotEqual(t, id1, id2)
assert.Len(t, id1, 36) // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
}
func TestResolveSpaceByName(t *testing.T) {
cfg := &Config{
Spaces: []SpaceConfig{
{Name: "alpha", URL: "https://alpha.example.com"},
{Name: "beta", URL: "https://beta.example.com"},
},
}
space, err := ResolveSpace(cfg, "beta")
require.NoError(t, err)
assert.Equal(t, "beta", space.Name)
assert.Equal(t, "https://beta.example.com", space.URL)
}
func TestResolveSpaceByNameNotFound(t *testing.T) {
cfg := &Config{
Spaces: []SpaceConfig{
{Name: "alpha", URL: "https://alpha.example.com"},
},
}
_, err := ResolveSpace(cfg, "nonexistent")
assert.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestResolveSpaceSingle(t *testing.T) {
cfg := &Config{
Spaces: []SpaceConfig{
{Name: "only", URL: "https://only.example.com"},
},
}
space, err := ResolveSpace(cfg, "")
require.NoError(t, err)
assert.Equal(t, "only", space.Name)
}
func TestResolveSpaceAmbiguous(t *testing.T) {
cfg := &Config{
Spaces: []SpaceConfig{
{Name: "alpha", URL: "https://alpha.example.com"},
{Name: "beta", URL: "https://beta.example.com"},
},
}
_, err := ResolveSpace(cfg, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "multiple spaces")
}
func TestResolveSpaceEmpty(t *testing.T) {
cfg := &Config{
Spaces: []SpaceConfig{},
}
_, err := ResolveSpace(cfg, "")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no spaces configured")
}