Add client engine and desktop CLI that mirror KDBX files
Why: a KeePass application must see an ordinary .kdbx file. A mirror directory of real files gives that on every desktop platform without FUSE, WinFsp or macFUSE, and without cgo. What: internal/client holds the API wrapper, an SSE reader, and a mirror agent whose entire policy is one pure decide() table: adopt, pull, push, force-push, server-wins, restore or manual. Saves are detected by rescan, remote changes arrive over the event stream, and every overwrite of local bytes leaves a conflict copy. cmd/keeporg drives it: login (password, TOTP or browser/OIDC device flow), run, sync, status, plus the group, grant, database and invite administration. Proved: go test ./internal/client covers the decision table and runs a real server in-process for adopt, follow, server-wins, client-wins, manual, read-only restore, server-side rename and event-driven update. Also smoke tested live: built both binaries, bootstrapped a server, logged in, created a database and adopted a local file.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
package client
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDecideTable(t *testing.T) {
|
||||
// A database this client has synced at version 2, digest "a".
|
||||
base := situation{LocalExists: true, LocalSHA: "a", SyncedSHA: "a", SyncedVersion: 2,
|
||||
RemoteSHA: "a", RemoteVersion: 2, CanWrite: true, Strategy: "server-wins"}
|
||||
|
||||
with := func(f func(*situation)) situation {
|
||||
s := base
|
||||
f(&s)
|
||||
return s
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
s situation
|
||||
want action
|
||||
}{
|
||||
{"settled", base, actNothing},
|
||||
{"local save", with(func(s *situation) { s.LocalSHA = "b" }), actPush},
|
||||
{"remote update", with(func(s *situation) { s.RemoteSHA, s.RemoteVersion = "c", 3 }), actPull},
|
||||
{"missing local file", with(func(s *situation) { s.LocalExists = false }), actPull},
|
||||
{"missing local file, empty database", situation{RemoteVersion: 0}, actNothing},
|
||||
{"adopt a local file into an empty database",
|
||||
situation{LocalExists: true, LocalSHA: "a", CanWrite: true, Strategy: "server-wins"}, actPush},
|
||||
{"both changed, server wins", with(func(s *situation) {
|
||||
s.LocalSHA, s.RemoteSHA, s.RemoteVersion = "b", "c", 3
|
||||
}), actServerWins},
|
||||
{"both changed, client wins", with(func(s *situation) {
|
||||
s.LocalSHA, s.RemoteSHA, s.RemoteVersion, s.Strategy = "b", "c", 3, "client-wins"
|
||||
}), actForcePush},
|
||||
{"both changed, manual", with(func(s *situation) {
|
||||
s.LocalSHA, s.RemoteSHA, s.RemoteVersion, s.Strategy = "b", "c", 3, "manual"
|
||||
}), actManual},
|
||||
{"manual conflict already reported", with(func(s *situation) {
|
||||
s.LocalSHA, s.RemoteSHA, s.RemoteVersion, s.Strategy = "b", "c", 3, "manual"
|
||||
s.ConflictSeen = true
|
||||
}), actNothing},
|
||||
{"local save without write access", with(func(s *situation) {
|
||||
s.LocalSHA, s.CanWrite = "b", false
|
||||
}), actRestore},
|
||||
{"both changed without write access", with(func(s *situation) {
|
||||
s.LocalSHA, s.RemoteSHA, s.RemoteVersion, s.CanWrite = "b", "c", 3, false
|
||||
}), actRestore},
|
||||
{"version moved but content identical", with(func(s *situation) {
|
||||
s.RemoteVersion = 3
|
||||
}), actPull},
|
||||
} {
|
||||
if got := decide(tc.s); got != tc.want {
|
||||
t.Errorf("%s: decide = %s, want %s", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user