* Replaced initial node.js prototype of CLI with proper Go implementation * Added code signing for CLI + server binaries for macOS
288 lines
7.3 KiB
Go
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")
|
|
}
|