fix: bind sync state to active vault
This commit is contained in:
parent
b845e8b5e2
commit
18da09e06b
|
|
@ -108,6 +108,9 @@ func NewApp(
|
||||||
debug: debugEnabled,
|
debug: debugEnabled,
|
||||||
activityEvents: make(map[string]bool),
|
activityEvents: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
if app.syncSvc == nil {
|
||||||
|
app.rebindSyncService()
|
||||||
|
}
|
||||||
app.ensureActivityProviderSubscriptions()
|
app.ensureActivityProviderSubscriptions()
|
||||||
app.startFileWatcherForOpenVault()
|
app.startFileWatcherForOpenVault()
|
||||||
return app
|
return app
|
||||||
|
|
@ -714,6 +717,7 @@ func (a *App) CreateVault(path string) error {
|
||||||
if err := a.vault.CreateVault(path); err != nil {
|
if err := a.vault.CreateVault(path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
a.rebindSyncService()
|
||||||
a.secretsSession = nil
|
a.secretsSession = nil
|
||||||
a.startFileWatcherForOpenVault()
|
a.startFileWatcherForOpenVault()
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -727,6 +731,7 @@ func (a *App) OpenVault(path string) error {
|
||||||
if err := a.vault.OpenVault(path); err != nil {
|
if err := a.vault.OpenVault(path); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
a.rebindSyncService()
|
||||||
a.secretsSession = nil
|
a.secretsSession = nil
|
||||||
a.startFileWatcherForOpenVault()
|
a.startFileWatcherForOpenVault()
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -741,6 +746,7 @@ func (a *App) CloseVault() error {
|
||||||
a.fileWatcher.Stop()
|
a.fileWatcher.Stop()
|
||||||
}
|
}
|
||||||
a.vault.CloseVault()
|
a.vault.CloseVault()
|
||||||
|
a.syncSvc = nil
|
||||||
a.secretsSession = nil
|
a.secretsSession = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -1656,6 +1662,7 @@ func (a *App) SetCurrentVault(path string) string {
|
||||||
a.secretsSession = nil
|
a.secretsSession = nil
|
||||||
// Save the actual vault path (normalized by OpenVault, includes VerstakVault/)
|
// Save the actual vault path (normalized by OpenVault, includes VerstakVault/)
|
||||||
vaultPath := a.vault.GetVaultPath()
|
vaultPath := a.vault.GetVaultPath()
|
||||||
|
a.rebindSyncService()
|
||||||
if err := a.appSettings.SetCurrentVault(vaultPath); err != nil {
|
if err := a.appSettings.SetCurrentVault(vaultPath); err != nil {
|
||||||
return fmt.Sprintf("failed to save app settings: %v", err)
|
return fmt.Sprintf("failed to save app settings: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -1685,6 +1692,16 @@ func (a *App) SetCurrentVault(path string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) rebindSyncService() {
|
||||||
|
if a == nil || a.vault == nil || a.vault.GetVaultStatus() != vault.StatusOpen {
|
||||||
|
if a != nil {
|
||||||
|
a.syncSvc = nil
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
a.syncSvc = syncsvc.NewService(a.vault.GetVaultPath(), "")
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) startFileWatcherForOpenVault() {
|
func (a *App) startFileWatcherForOpenVault() {
|
||||||
if a == nil || a.vault == nil || a.eventBus == nil {
|
if a == nil || a.vault == nil || a.eventBus == nil {
|
||||||
return
|
return
|
||||||
|
|
@ -2160,7 +2177,9 @@ func (a *App) syncStatus() (*SyncStatusDTO, error) {
|
||||||
LastError: cfg.Sync.LastError,
|
LastError: cfg.Sync.LastError,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Sync.DeviceID != "" {
|
if deviceID := a.syncSvc.GetDeviceID(); deviceID != "" {
|
||||||
|
dto.DeviceID = deviceID
|
||||||
|
} else if cfg.Sync.DeviceID != "" {
|
||||||
dto.DeviceID = cfg.Sync.DeviceID
|
dto.DeviceID = cfg.Sync.DeviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2170,10 +2189,13 @@ func (a *App) syncStatus() (*SyncStatusDTO, error) {
|
||||||
if deviceToken != "" {
|
if deviceToken != "" {
|
||||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||||
client.DeviceToken = deviceToken
|
client.DeviceToken = deviceToken
|
||||||
if cfg.Sync.DeviceID != "" {
|
if dto.DeviceID != "" {
|
||||||
client.DeviceID = cfg.Sync.DeviceID
|
client.DeviceID = dto.DeviceID
|
||||||
}
|
}
|
||||||
if info, err := client.GetMe(); err == nil {
|
if info, err := client.GetMe(); err == nil {
|
||||||
|
if info.DeviceID != "" {
|
||||||
|
_ = a.syncSvc.SetDeviceID(info.DeviceID)
|
||||||
|
}
|
||||||
dto.DeviceName = info.DeviceName
|
dto.DeviceName = info.DeviceName
|
||||||
dto.DeviceID = info.DeviceID
|
dto.DeviceID = info.DeviceID
|
||||||
dto.Connected = true
|
dto.Connected = true
|
||||||
|
|
@ -2224,19 +2246,24 @@ func (a *App) syncConfigure(serverURL, username, password string) error {
|
||||||
if err := a.requireVault(); err != nil {
|
if err := a.requireVault(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
meta := a.vault.GetVaultMeta()
|
||||||
|
if meta == nil || strings.TrimSpace(meta.VaultID) == "" {
|
||||||
|
return fmt.Errorf("vault ID is unavailable")
|
||||||
|
}
|
||||||
vaultPath := a.vaultPath()
|
vaultPath := a.vaultPath()
|
||||||
hostname, _ := os.Hostname()
|
hostname, _ := os.Hostname()
|
||||||
if hostname == "" {
|
if hostname == "" {
|
||||||
hostname = "unknown"
|
hostname = "unknown"
|
||||||
}
|
}
|
||||||
client := newSyncClient(serverURL, "", "", vaultPath)
|
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||||
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-desktop/v2")
|
deviceID, deviceToken, err := client.PairDevice(serverURL, username, password, hostname, "verstak-desktop/v2", meta.VaultID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("pair: %w", err)
|
return fmt.Errorf("pair: %w", err)
|
||||||
}
|
}
|
||||||
if err := syncsvc.SaveDeviceToken(vaultPath, deviceToken); err != nil {
|
if err := syncsvc.SaveDeviceToken(vaultPath, deviceToken); err != nil {
|
||||||
return fmt.Errorf("save token: %w", err)
|
return fmt.Errorf("save token: %w", err)
|
||||||
}
|
}
|
||||||
|
a.syncSvc = syncsvc.NewService(vaultPath, deviceID)
|
||||||
if err := a.syncSvc.SetState(serverURL, ""); err != nil {
|
if err := a.syncSvc.SetState(serverURL, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -2271,9 +2298,15 @@ func (a *App) syncDisconnect() error {
|
||||||
vaultPath := a.vaultPath()
|
vaultPath := a.vaultPath()
|
||||||
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
||||||
cfg := a.appSettings.Get()
|
cfg := a.appSettings.Get()
|
||||||
|
serverURL := cfg.Sync.ServerURL
|
||||||
|
if a.syncSvc != nil {
|
||||||
|
if currentServerURL, _, _, _, err := a.syncSvc.GetState(); err == nil && currentServerURL != "" {
|
||||||
|
serverURL = currentServerURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if deviceToken != "" {
|
if deviceToken != "" && serverURL != "" {
|
||||||
client := newSyncClient(cfg.Sync.ServerURL, "", "", vaultPath)
|
client := newSyncClient(serverURL, "", "", vaultPath)
|
||||||
client.DeviceToken = deviceToken
|
client.DeviceToken = deviceToken
|
||||||
_ = client.RevokeCurrent()
|
_ = client.RevokeCurrent()
|
||||||
}
|
}
|
||||||
|
|
@ -2288,6 +2321,9 @@ func (a *App) syncDisconnect() error {
|
||||||
if err := a.appSettings.UpdateSync(cfg.Sync); err != nil {
|
if err := a.appSettings.UpdateSync(cfg.Sync); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if a.syncSvc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return a.syncSvc.SetState("", "")
|
return a.syncSvc.SetState("", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2350,6 +2386,7 @@ func (a *App) syncResetKey() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
vaultPath := a.vaultPath()
|
vaultPath := a.vaultPath()
|
||||||
|
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
||||||
if err := syncsvc.RemoveDeviceToken(vaultPath); err != nil && !os.IsNotExist(err) {
|
if err := syncsvc.RemoveDeviceToken(vaultPath); err != nil && !os.IsNotExist(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -2361,7 +2398,14 @@ func (a *App) syncResetKey() error {
|
||||||
cfg.Sync.LastStatus = "disconnected"
|
cfg.Sync.LastStatus = "disconnected"
|
||||||
cfg.Sync.LastError = ""
|
cfg.Sync.LastError = ""
|
||||||
if a.syncSvc != nil {
|
if a.syncSvc != nil {
|
||||||
if err := a.syncSvc.SetState(cfg.Sync.ServerURL, ""); err != nil {
|
serverURL, _, _, _, err := a.syncSvc.GetState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if serverURL == "" && deviceToken != "" {
|
||||||
|
serverURL = cfg.Sync.ServerURL
|
||||||
|
}
|
||||||
|
if err := a.syncSvc.SetState(serverURL, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2384,6 +2428,12 @@ func (a *App) syncNow() (map[string]interface{}, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
vaultPath := a.vaultPath()
|
vaultPath := a.vaultPath()
|
||||||
|
if a.syncSvc == nil {
|
||||||
|
a.rebindSyncService()
|
||||||
|
}
|
||||||
|
if a.syncSvc == nil {
|
||||||
|
return nil, fmt.Errorf("sync service not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
serverURL, apiKey, lastPullSeq, _, err := a.syncSvc.GetState()
|
serverURL, apiKey, lastPullSeq, _, err := a.syncSvc.GetState()
|
||||||
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
deviceToken := syncsvc.LoadDeviceToken(vaultPath)
|
||||||
|
|
@ -2391,14 +2441,28 @@ func (a *App) syncNow() (map[string]interface{}, error) {
|
||||||
return nil, fmt.Errorf("sync not configured")
|
return nil, fmt.Errorf("sync not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceID := ""
|
deviceID := a.syncSvc.GetDeviceID()
|
||||||
cfg := a.appSettings.Get()
|
cfg := a.appSettings.Get()
|
||||||
if cfg.Sync.DeviceID != "" {
|
if deviceID == "" && deviceToken == "" && cfg.Sync.DeviceID != "" {
|
||||||
deviceID = cfg.Sync.DeviceID
|
deviceID = cfg.Sync.DeviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
client := newSyncClient(serverURL, apiKey, deviceID, vaultPath)
|
client := newSyncClient(serverURL, apiKey, deviceID, vaultPath)
|
||||||
client.DeviceToken = deviceToken
|
client.DeviceToken = deviceToken
|
||||||
|
if deviceID == "" && deviceToken != "" {
|
||||||
|
info, err := client.GetMe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("sync identity: %w", err)
|
||||||
|
}
|
||||||
|
if info.DeviceID == "" {
|
||||||
|
return nil, fmt.Errorf("sync identity: server returned an empty device ID")
|
||||||
|
}
|
||||||
|
if err := a.syncSvc.SetDeviceID(info.DeviceID); err != nil {
|
||||||
|
return nil, fmt.Errorf("save sync identity: %w", err)
|
||||||
|
}
|
||||||
|
deviceID = info.DeviceID
|
||||||
|
client.DeviceID = deviceID
|
||||||
|
}
|
||||||
|
|
||||||
unpushed, err := a.syncSvc.GetUnpushedOps()
|
unpushed, err := a.syncSvc.GetUnpushedOps()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -2637,14 +2701,19 @@ func syncPayloadPath(op syncsvc.Op, payload syncFilePayload) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) localSyncDeviceID() string {
|
func (a *App) localSyncDeviceID() string {
|
||||||
|
if a.syncSvc != nil {
|
||||||
|
if deviceID := a.syncSvc.GetDeviceID(); deviceID != "" {
|
||||||
|
return deviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.vault != nil && a.vault.GetVaultStatus() == vault.StatusOpen && syncsvc.LoadDeviceToken(a.vaultPath()) != "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
if a.appSettings != nil {
|
if a.appSettings != nil {
|
||||||
if deviceID := a.appSettings.Get().Sync.DeviceID; deviceID != "" {
|
if deviceID := a.appSettings.Get().Sync.DeviceID; deviceID != "" {
|
||||||
return deviceID
|
return deviceID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a.syncSvc != nil {
|
|
||||||
return a.syncSvc.GetDeviceID()
|
|
||||||
}
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1104,6 +1104,174 @@ func TestSyncNowPushesLocalOpsAndAppliesPulledFileOps(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSyncConfigurePairsCurrentVaultID(t *testing.T) {
|
||||||
|
app, _ := newSyncFilesTestApp(t, []string{"files.read", "files.write", "files.delete"}, "local-device")
|
||||||
|
meta := app.vault.GetVaultMeta()
|
||||||
|
if meta == nil || meta.VaultID == "" {
|
||||||
|
t.Fatal("test vault must have an ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var pairedVaultID string
|
||||||
|
server := newLocalHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/client/pair" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request struct {
|
||||||
|
VaultID string `json:"vault_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pairedVaultID = request.VaultID
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"device_id": "paired-device",
|
||||||
|
"device_token": "paired-token",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
if err := app.syncConfigure(server.URL, "alice", "secret"); err != nil {
|
||||||
|
t.Fatalf("syncConfigure: %v", err)
|
||||||
|
}
|
||||||
|
if pairedVaultID != meta.VaultID {
|
||||||
|
t.Fatalf("paired vault ID = %q, want %q", pairedVaultID, meta.VaultID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncNowHydratesLegacyVaultDeviceID(t *testing.T) {
|
||||||
|
app, root := newSyncFilesTestApp(t, []string{"files.read", "files.write", "files.delete"}, "wrong-global-device")
|
||||||
|
var pushedDeviceID string
|
||||||
|
server := newLocalHTTPTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Header.Get("Authorization") != "Bearer vault-token" {
|
||||||
|
http.Error(w, "missing auth", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/client/me":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"device_id": "vault-device"})
|
||||||
|
case "/api/v1/sync/push":
|
||||||
|
var request struct {
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
Ops []struct {
|
||||||
|
OpID string `json:"op_id"`
|
||||||
|
} `json:"ops"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pushedDeviceID = request.DeviceID
|
||||||
|
accepted := make([]string, 0, len(request.Ops))
|
||||||
|
for _, op := range request.Ops {
|
||||||
|
accepted = append(accepted, op.OpID)
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"accepted": accepted,
|
||||||
|
"count": len(accepted),
|
||||||
|
"conflicts": []map[string]interface{}{},
|
||||||
|
})
|
||||||
|
case "/api/v1/sync/pull":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"server_sequence": 0,
|
||||||
|
"ops": []map[string]interface{}{},
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
app.syncSvc = syncsvc.NewService(root, "")
|
||||||
|
if err := app.syncSvc.SetState(server.URL, ""); err != nil {
|
||||||
|
t.Fatalf("SetState: %v", err)
|
||||||
|
}
|
||||||
|
if err := syncsvc.SaveDeviceToken(root, "vault-token"); err != nil {
|
||||||
|
t.Fatalf("SaveDeviceToken: %v", err)
|
||||||
|
}
|
||||||
|
if err := app.syncSvc.RecordOp(syncsvc.EntityFile, "Docs/one.txt", syncsvc.OpCreate, map[string]string{"path": "Docs/one.txt"}); err != nil {
|
||||||
|
t.Fatalf("RecordOp: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := app.syncNow(); err != nil {
|
||||||
|
t.Fatalf("syncNow: %v", err)
|
||||||
|
}
|
||||||
|
if pushedDeviceID != "vault-device" {
|
||||||
|
t.Fatalf("pushed device ID = %q, want vault-device", pushedDeviceID)
|
||||||
|
}
|
||||||
|
if app.syncSvc.GetDeviceID() != "vault-device" {
|
||||||
|
t.Fatalf("persisted device ID = %q, want vault-device", app.syncSvc.GetDeviceID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVaultTransitionsRebindSyncService(t *testing.T) {
|
||||||
|
settings := appsettings.NewManager(filepath.Join(t.TempDir(), "config.json"))
|
||||||
|
if err := settings.Load(); err != nil {
|
||||||
|
t.Fatalf("settings Load: %v", err)
|
||||||
|
}
|
||||||
|
app := &App{
|
||||||
|
capRegistry: capability.NewRegistry(),
|
||||||
|
vault: vault.NewVault(nil),
|
||||||
|
appSettings: settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.CreateVault(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("CreateVault: %v", err)
|
||||||
|
}
|
||||||
|
if app.syncSvc == nil {
|
||||||
|
t.Fatal("CreateVault must initialize the sync service")
|
||||||
|
}
|
||||||
|
|
||||||
|
vaultB := vault.NewVault(nil)
|
||||||
|
if err := vaultB.CreateVault(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("Create vault B: %v", err)
|
||||||
|
}
|
||||||
|
primeSyncState(t, vaultB.GetVaultPath(), "device-b", "https://sync-b.example.test", 22)
|
||||||
|
if err := app.OpenVault(vaultB.GetVaultPath()); err != nil {
|
||||||
|
t.Fatalf("OpenVault B: %v", err)
|
||||||
|
}
|
||||||
|
assertSyncState(t, app.syncSvc, "device-b", "https://sync-b.example.test", 22)
|
||||||
|
|
||||||
|
vaultC := vault.NewVault(nil)
|
||||||
|
if err := vaultC.CreateVault(t.TempDir()); err != nil {
|
||||||
|
t.Fatalf("Create vault C: %v", err)
|
||||||
|
}
|
||||||
|
primeSyncState(t, vaultC.GetVaultPath(), "device-c", "https://sync-c.example.test", 33)
|
||||||
|
if errStr := app.SetCurrentVault(vaultC.GetVaultPath()); errStr != "" {
|
||||||
|
t.Fatalf("SetCurrentVault C: %s", errStr)
|
||||||
|
}
|
||||||
|
assertSyncState(t, app.syncSvc, "device-c", "https://sync-c.example.test", 33)
|
||||||
|
}
|
||||||
|
|
||||||
|
func primeSyncState(t *testing.T, vaultRoot, deviceID, serverURL string, lastPullSeq int) {
|
||||||
|
t.Helper()
|
||||||
|
service := syncsvc.NewService(vaultRoot, deviceID)
|
||||||
|
if err := service.SetState(serverURL, ""); err != nil {
|
||||||
|
t.Fatalf("SetState: %v", err)
|
||||||
|
}
|
||||||
|
if err := service.SetLastPullSeq(lastPullSeq); err != nil {
|
||||||
|
t.Fatalf("SetLastPullSeq: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSyncState(t *testing.T, service *syncsvc.Service, wantDeviceID, wantServerURL string, wantLastPullSeq int) {
|
||||||
|
t.Helper()
|
||||||
|
if service == nil {
|
||||||
|
t.Fatal("sync service is nil")
|
||||||
|
}
|
||||||
|
serverURL, _, lastPullSeq, _, err := service.GetState()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetState: %v", err)
|
||||||
|
}
|
||||||
|
if service.GetDeviceID() != wantDeviceID || serverURL != wantServerURL || lastPullSeq != wantLastPullSeq {
|
||||||
|
t.Fatalf("sync state = device=%q server=%q cursor=%d, want device=%q server=%q cursor=%d",
|
||||||
|
service.GetDeviceID(), serverURL, lastPullSeq, wantDeviceID, wantServerURL, wantLastPullSeq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSetCurrentVaultInitializesWorkspaceWhenMissingAtStartup(t *testing.T) {
|
func TestSetCurrentVaultInitializesWorkspaceWhenMissingAtStartup(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
vaultParent := filepath.Join(tmpDir, "vault-parent")
|
vaultParent := filepath.Join(tmpDir, "vault-parent")
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,13 @@ func NewClient(serverURL, apiKey, deviceID, vaultRoot string) *Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
// PairDevice calls POST /api/client/pair and returns device_id and device_token.
|
// PairDevice calls POST /api/client/pair and returns device_id and device_token.
|
||||||
func (c *Client) PairDevice(serverURL, username, password, deviceName, clientVersion string) (deviceID, deviceToken string, err error) {
|
func (c *Client) PairDevice(serverURL, username, password, deviceName, clientVersion, vaultID string) (deviceID, deviceToken string, err error) {
|
||||||
body := map[string]string{
|
body := map[string]string{
|
||||||
"login": username,
|
"login": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
"device_name": deviceName,
|
"device_name": deviceName,
|
||||||
"client_version": clientVersion,
|
"client_version": clientVersion,
|
||||||
|
"vault_id": vaultID,
|
||||||
}
|
}
|
||||||
var resp struct {
|
var resp struct {
|
||||||
DeviceID string `json:"device_id"`
|
DeviceID string `json:"device_id"`
|
||||||
|
|
|
||||||
|
|
@ -86,3 +86,36 @@ func TestPushDoesNotRetryClientErrors(t *testing.T) {
|
||||||
t.Fatalf("attempts = %d, want 1", attempts)
|
t.Fatalf("attempts = %d, want 1", attempts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPairDeviceSendsVaultID(t *testing.T) {
|
||||||
|
var pairedVaultID string
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/client/pair" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request struct {
|
||||||
|
VaultID string `json:"vault_id"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pairedVaultID = request.VaultID
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"device_id": "device-1",
|
||||||
|
"device_token": "token-1",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := NewClient(server.URL, "", "", t.TempDir())
|
||||||
|
_, _, err := client.PairDevice(server.URL, "alice", "secret", "Desktop", "verstak-desktop/v2", "vault-123")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PairDevice: %v", err)
|
||||||
|
}
|
||||||
|
if pairedVaultID != "vault-123" {
|
||||||
|
t.Fatalf("paired vault ID = %q, want vault-123", pairedVaultID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,13 @@ type Service struct {
|
||||||
|
|
||||||
// NewService creates a sync service.
|
// NewService creates a sync service.
|
||||||
func NewService(vaultRoot, deviceID string) *Service {
|
func NewService(vaultRoot, deviceID string) *Service {
|
||||||
return &Service{vaultRoot: vaultRoot, deviceID: deviceID}
|
service := &Service{vaultRoot: vaultRoot, deviceID: deviceID}
|
||||||
|
if deviceID == "" {
|
||||||
|
if state, err := service.loadState(); err == nil {
|
||||||
|
service.deviceID = state.DeviceID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) syncDir() string {
|
func (s *Service) syncDir() string {
|
||||||
|
|
@ -212,6 +218,9 @@ func (s *Service) SetState(serverURL, apiKey string) error {
|
||||||
}
|
}
|
||||||
st.ServerURL = serverURL
|
st.ServerURL = serverURL
|
||||||
st.APIKey = apiKey
|
st.APIKey = apiKey
|
||||||
|
if s.deviceID != "" {
|
||||||
|
st.DeviceID = s.deviceID
|
||||||
|
}
|
||||||
return s.saveState(st)
|
return s.saveState(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,6 +249,23 @@ func (s *Service) GetDeviceID() string {
|
||||||
return s.deviceID
|
return s.deviceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDeviceID persists the device ID for this vault's sync state.
|
||||||
|
func (s *Service) SetDeviceID(deviceID string) error {
|
||||||
|
if err := s.ensureDir(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
st, err := s.loadState()
|
||||||
|
if err != nil {
|
||||||
|
st = &syncState{}
|
||||||
|
}
|
||||||
|
st.DeviceID = deviceID
|
||||||
|
if err := s.saveState(st); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.deviceID = deviceID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- file helpers ---
|
// --- file helpers ---
|
||||||
|
|
||||||
func (s *Service) loadOps() ([]Op, error) {
|
func (s *Service) loadOps() ([]Op, error) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue