mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
Authentication (previous phase, now landed together with its follow-ups): - Email+password+name accounts behind an AuthProvider interface; the OSS server ships BuiltinAuth only (file-backed auth.json: bcrypt password hashes + SHA-256 token digests, plaintext never stored; server-owned /auth/* pages; managed deployments can swap in another provider). - bdrive login: loopback-callback browser flow (sign-up on the page, the terminal finishes itself) with a device-code fallback for headless machines; long-lived revocable device tokens in settings.json. - Password reset via plain SMTP (stdlib) with a log-link fallback when no SMTP is configured. Move-proof projects: - .bdrive is now a directory; config.json carries a stable mount id. The volume store (~/.bdrive/volumes/<mount-id>/) and registry are keyed by that id — never the folder path — so renames/moves are free. - The daemon re-reads the project config each tick and exits cleanly (propagating nothing) when its folder vanishes; the registry self-heals and the next bdrive command at the new location resumes with zero spurious changes. bdrive init is the front door (mnt/umnt removed; bdrive stop pauses): - Interactive on a TTY (create new / connect existing project from the server's list; whole folder / shared subfolder via the include list), full flag bypass (--name/--project/--shared/--yes), never prompts without a TTY. Runs the login flow first when there is no session. Default server: beardrive.ai (config.DefaultServer). Web history (revert-ready): - Hubs now always require auth; journal ops carry the signed-in account (user/user_name) alongside the git/OS fallback author. - File-backed device registry: per-device name, OS, account, and the public IP the server observed, joined into history at read time. - GET /api/p/<id>/history?path=|prefix= (newest first) and GET /api/p/<id>/blob?sha= stream any exact version — blobs are retained forever, so the next phase's revert is re-putting an old blob. - UI: History button (file versions or project feed), per-folder history shortcut, view/download of any past version. Tests: auth flows (callback, device-code, reset single-use, persistence, gating), history API + device registry, folder-move survival, registry self-heal, ops-carry-account; docs (README/SKILL/CLAUDE) updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R7Q9ZKSZRTdvrSJkYLUmYs
103 lines
3.0 KiB
Go
103 lines
3.0 KiB
Go
package syncer
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/runbear-io/beardrive/internal/config"
|
|
)
|
|
|
|
// A renamed/moved folder must keep syncing seamlessly: state is keyed by the
|
|
// stable mount id, never by the path, so the move produces zero spurious ops
|
|
// and later edits sync normally.
|
|
func TestFolderMoveKeepsSyncing(t *testing.T) {
|
|
be := sharedRemote(t)
|
|
a := newDevice(t, "deva", be)
|
|
a.MountID = "m-test0001"
|
|
b := newDevice(t, "devb", be)
|
|
|
|
write(t, a.Folder, "docs/spec.md", "v1")
|
|
cycle(t, a)
|
|
cycle(t, b)
|
|
if read(t, b.Folder, "docs/spec.md") != "v1" {
|
|
t.Fatal("initial sync failed")
|
|
}
|
|
|
|
// Rename/move the folder; the session keeps the same mount id, so the
|
|
// state cache still matches — the move itself is not a change.
|
|
moved := filepath.Join(t.TempDir(), "renamed-project")
|
|
if err := os.Rename(a.Folder, moved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a2 := &Session{Folder: moved, MountID: a.MountID, Store: a.Store, Device: a.Device, Backend: be}
|
|
res := cycle(t, a2)
|
|
if res.LocalOps != 0 || res.Materialized != 0 {
|
|
t.Fatalf("move must be invisible to sync, got %+v", res)
|
|
}
|
|
|
|
// Edits at the new location keep syncing.
|
|
time.Sleep(10 * time.Millisecond)
|
|
write(t, moved, "docs/spec.md", "v2 after move")
|
|
cycle(t, a2)
|
|
cycle(t, b)
|
|
if got := read(t, b.Folder, "docs/spec.md"); got != "v2 after move" {
|
|
t.Fatalf("post-move edit did not sync: %q", got)
|
|
}
|
|
}
|
|
|
|
// Ops written by a logged-in device carry the account.
|
|
func TestOpsCarryAccount(t *testing.T) {
|
|
a := newDevice(t, "deva", nil)
|
|
a.Account = config.Settings{Email: "alice@x.io", Name: "Alice"}
|
|
write(t, a.Folder, "note.md", "hello")
|
|
cycle(t, a)
|
|
ops, err := a.Store.DeviceOps(a.Device.ID)
|
|
if err != nil || len(ops) != 1 {
|
|
t.Fatalf("ops = %v, %v", ops, err)
|
|
}
|
|
if ops[0].User != "alice@x.io" || ops[0].UserName != "Alice" {
|
|
t.Fatalf("op identity = %+v, want the signed-in account", ops[0])
|
|
}
|
|
if ops[0].Author == "" {
|
|
t.Fatal("git/OS fallback identity should still be present")
|
|
}
|
|
}
|
|
|
|
// The registry self-heals: ResolveMount at the folder's new path updates the
|
|
// path the daemon and status use.
|
|
func TestRegistrySelfHeal(t *testing.T) {
|
|
t.Setenv("BDRIVE_HOME", t.TempDir())
|
|
folder := filepath.Join(t.TempDir(), "proj")
|
|
if err := os.MkdirAll(folder, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
proj, err := config.SaveProject(folder, config.Project{Volume: "demo", Remote: "file:///tmp/x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if proj.ID == "" {
|
|
t.Fatal("SaveProject must assign a mount id")
|
|
}
|
|
if _, _, err := config.ResolveMount(folder); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
moved := filepath.Join(t.TempDir(), "proj-renamed")
|
|
if err := os.Rename(folder, moved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, ok, err := config.ResolveMount(moved)
|
|
if err != nil || !ok || got.ID != proj.ID {
|
|
t.Fatalf("resolve after move = %+v %v %v", got, ok, err)
|
|
}
|
|
mounts, err := config.LoadMounts()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if mounts[proj.ID].Path != moved {
|
|
t.Fatalf("registry path = %q, want %q", mounts[proj.ID].Path, moved)
|
|
}
|
|
}
|