Why: collaborative KeePass use needs one arbiter of "who wrote last" that never sees a master password, plus access control that follows an org chart. What: stdlib-only Go server. Metadata is a single JSON document with one writer and atomic commit; KDBX payloads are versioned blobs on disk. Access comes from a group tree where a grant on a group reaches every member below it. Login is password+TOTP, invite links, or OIDC through a browser-approved device flow. Pushes carry If-Match and are arbitrated by a per-database merge strategy (server-wins by default). Updates are pushed to clients over SSE. Proved: go test ./internal/... covers RFC 6238 vectors, ACL inheritance and isolation, store rollback, blob pruning, every push verdict, and an end-to-end HTTP pass over login, conflict, force, read-only grants, invite redemption, TOTP enrolment, device login and the event stream.
77 lines
1.8 KiB
Go
77 lines
1.8 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestUpdatePersistsAndSurvivesReopen(t *testing.T) {
|
|
p := filepath.Join(t.TempDir(), "state.json")
|
|
st, err := Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.Update(func(s *State) error {
|
|
s.Users["u1"] = &User{ID: "u1", Username: "alice"}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again, err := Open(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var name string
|
|
again.Read(func(s *State) { name = s.Users["u1"].Username })
|
|
if name != "alice" {
|
|
t.Fatalf("reopened username = %q", name)
|
|
}
|
|
if fi, _ := os.Stat(p); fi.Mode().Perm() != 0o600 {
|
|
t.Fatalf("state file mode = %v", fi.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestFailedUpdateLeavesStateUntouched(t *testing.T) {
|
|
st, err := Open(filepath.Join(t.TempDir(), "state.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
boom := errors.New("boom")
|
|
if err := st.Update(func(s *State) error {
|
|
s.Users["u1"] = &User{ID: "u1"}
|
|
return boom
|
|
}); !errors.Is(err, boom) {
|
|
t.Fatalf("err = %v", err)
|
|
}
|
|
var n int
|
|
st.Read(func(s *State) { n = len(s.Users) })
|
|
if n != 0 {
|
|
t.Fatalf("users after failed update = %d", n)
|
|
}
|
|
}
|
|
|
|
func TestGCDropsExpired(t *testing.T) {
|
|
st, _ := Open(filepath.Join(t.TempDir(), "state.json"))
|
|
now := time.Now()
|
|
_ = st.Update(func(s *State) error {
|
|
s.Sessions["old"] = &Session{Token: "old", ExpiresAt: now.Add(-time.Hour)}
|
|
s.Sessions["new"] = &Session{Token: "new", ExpiresAt: now.Add(time.Hour)}
|
|
s.Invites["spent"] = &Invite{Token: "spent", ExpiresAt: now.Add(time.Hour), MaxUses: 1, Uses: 1}
|
|
return nil
|
|
})
|
|
if err := st.GC(now); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st.Read(func(s *State) {
|
|
if len(s.Sessions) != 1 || s.Sessions["new"] == nil {
|
|
t.Fatalf("sessions after GC = %v", s.Sessions)
|
|
}
|
|
if len(s.Invites) != 0 {
|
|
t.Fatalf("invites after GC = %v", s.Invites)
|
|
}
|
|
})
|
|
}
|