Files
magpie/internal/davsync/davsync_test.go
T
Lin Huazhong d8ebf2b893 test: one list of the agent folders a sandbox clears, so no sandbox misses an agent (yetone, on #522: A shared list of env vars for the other sandboxes would be a welcome follow-up) — every sandbox kept its own list of the variables that point at an agent's folder, and each had drifted: the gateway's cleared 5 of the 32 magpie reads, the provider package's 2, the tui's 10, the library's 20. A sandbox clearing 2 meets whatever Grok, Kimi, Hermes, MiniMax, Qoder or ZCode the developer has installed, which is how #522 came about — Qoder's two were in the library's list and in none of the four that failed. internal/agentenv is now the one list, and the 21 sandboxes that fake an environment with no agent in it clear it, each keeping beside it the variables that are not an agent's: APPDATA and LOCALAPPDATA, which a sandbox sets to a folder of its own rather than clearing, VISUAL and EDITOR. A name in the list is one magpie reads, and TestVarsAreRead walks the sources to say so, which is how two names a sandbox had been clearing turned out to be read by nothing at all — GEMINI_CLI_HOME and OPENCODE_CONFIG in internal/backup's, while OPENCODE_CONFIG_DIR, the one internal/agent reads (agents.go:545), was not in it. Where a sandbox set an agent's folder to a fixture of its own, the clearing now comes first, since a list of 32 wipes what a list of 5 left standing. Not converted: the omp tests' four variables, which are the ones omp's own lookup consults (a Pi fork) rather than every agent's, and the warm-up's Claude Code traffic flags, which are not folders — CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC stays out of the list for that reason too, its one test setting it on purpose. On Windows 11 the whole suite answers as it did before the change: 31 packages pass, and the same 15 tests in internal/library, internal/gateway and internal/agent fail for what they failed for before (symlinks this machine has no privilege for, and TestSuffixModesReachAgentFiles). go vet ./..., go build -tags nogui (windows, GOOS=linux, GOOS=darwin), and go vet -tags nogui for linux and darwin over every package touched pass. (#543)
2026-10-02 17:22:16 +08:00

627 lines
21 KiB
Go

package davsync
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/yetone/magpie/internal/agentenv"
"github.com/yetone/magpie/internal/backup"
"github.com/yetone/magpie/internal/library"
"github.com/yetone/magpie/internal/profile"
"github.com/yetone/magpie/internal/provider"
"github.com/yetone/magpie/internal/settings"
)
// fakeDAV is a WebDAV server with one user, keeping files in memory.
type fakeDAV struct {
mu sync.Mutex
files map[string][]byte
etags map[string]string
dirs map[string]bool
n int
puts int
// nutstore: a file read in a folder that isn't there is a 409, as
// 坚果云 (Nutstore) answers, not a 404
nutstore bool
// cond: a read sent with the version last seen is answered 304 when
// it is still that one; putETag: a write's answer says its ETag;
// lastModified: files have a Last-Modified, a second apart for each
// write, and no ETag; tooMany: every request is answered 429, with
// retryAfter as its Retry-After
cond, putETag, lastModified bool
tooMany bool
retryAfter string
mtimes map[string]time.Time
// gets are the reads, full those answered with the file, of bytes
// bytes in all, and notModified those answered 304
gets, full, bytes, notModified int
}
func (f *fakeDAV) ServeHTTP(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()
if u, p, ok := r.BasicAuth(); !ok || u != "me" || p != "pw" {
w.WriteHeader(http.StatusUnauthorized)
return
}
if f.tooMany {
if f.retryAfter != "" {
w.Header().Set("Retry-After", f.retryAfter)
}
w.WriteHeader(http.StatusTooManyRequests)
return
}
switch r.Method {
case http.MethodGet:
f.gets++
b, ok := f.files[r.URL.Path]
if !ok && f.nutstore && !f.dirs[urlDir(r.URL.Path)] {
w.WriteHeader(http.StatusConflict)
return
}
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
if f.lastModified {
mt := f.mtimes[r.URL.Path]
w.Header().Set("Last-Modified", mt.Format(http.TimeFormat))
if ims, err := http.ParseTime(r.Header.Get("If-Modified-Since")); f.cond && err == nil && !mt.After(ims) {
f.notModified++
w.WriteHeader(http.StatusNotModified)
return
}
} else {
w.Header().Set("ETag", f.etags[r.URL.Path])
if f.cond && r.Header.Get("If-None-Match") == f.etags[r.URL.Path] {
f.notModified++
w.WriteHeader(http.StatusNotModified)
return
}
}
f.full++
f.bytes += len(b)
w.Write(b)
case http.MethodPut:
if !f.dirs[urlDir(r.URL.Path)] {
w.WriteHeader(http.StatusConflict)
return
}
if m := r.Header.Get("If-Match"); m != "" && m != f.etags[r.URL.Path] {
w.WriteHeader(http.StatusPreconditionFailed)
return
}
b, _ := io.ReadAll(r.Body)
f.n++
f.puts++
f.files[r.URL.Path], f.etags[r.URL.Path] = b, fmt.Sprintf(`"%d"`, f.n)
if f.mtimes == nil {
f.mtimes = map[string]time.Time{}
}
// long before now, and a second later for each write
f.mtimes[r.URL.Path] = time.Date(2026, 1, 1, 0, 0, f.n, 0, time.UTC)
if f.putETag && !f.lastModified {
w.Header().Set("ETag", f.etags[r.URL.Path])
}
w.WriteHeader(http.StatusCreated)
case "MKCOL":
p := strings.TrimSuffix(r.URL.Path, "/")
if f.dirs[p] {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
f.dirs[p] = true
w.WriteHeader(http.StatusCreated)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
// urlDir is the folder a URL path is in: a URL's, with / on every system
func urlDir(p string) string { return p[:max(strings.LastIndex(p, "/"), 1)] }
// computer is one machine's magpie: its own home, which the test moves
// between.
type computer string
func newComputer(t *testing.T) computer { return computer(t.TempDir()) }
func (c computer) use(t *testing.T) {
t.Setenv("HOME", string(c))
t.Setenv("USERPROFILE", string(c))
t.Setenv("XDG_CONFIG_HOME", filepath.Join(string(c), ".config"))
t.Setenv("XDG_CACHE_HOME", filepath.Join(string(c), ".cache"))
t.Setenv("PATH", "")
for _, v := range agentenv.Vars {
t.Setenv(v, "")
}
t.Setenv("APPDATA", "")
t.Setenv("LOCALAPPDATA", "")
t.Setenv("CLAUDE_CONFIG_DIR", filepath.Join(string(c), ".claude"))
t.Setenv("CODEX_HOME", filepath.Join(string(c), ".codex"))
}
func ids() []string {
var out []string
ps, _ := provider.Stored()
for _, p := range ps {
out = append(out, p.ID+"="+p.Key)
}
slices.Sort(out)
return out
}
func TestSync(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true}}
srv := httptest.NewServer(fake)
defer srv.Close()
ctx := context.Background()
cfg := Config{URL: srv.URL + "/dav/", User: "me", Password: "pw", Passphrase: "correct horse", Keys: true, Agents: true}
now := func(t *testing.T) View {
t.Helper()
if err := Now(ctx); err != nil {
t.Fatal(err)
}
return Status()
}
a, b := newComputer(t), newComputer(t)
a.use(t)
provider.Save(provider.Provider{ID: "deepseek", Name: "DeepSeek", Chat: "https://api.deepseek.com/v1", Key: "k1"})
settings.Save(settings.Settings{Theme: "dark", Proxy: "http://127.0.0.1:7890", Window: []int{900, 700}, TrayUsages: []string{"claude|a@b.c"}, TrayUsageEvery: 5, TrayNoLogos: true})
profile.Save("work", profile.Profile{Fields: map[string]string{"claude.model": "x"}})
same := cfg
same.Passphrase = cfg.Password
if err := Configure(same); err == nil || !strings.Contains(err.Error(), "of its own") {
t.Fatalf("a passphrase that is the password: %v", err)
}
if err := Configure(cfg); err != nil {
t.Fatal(err)
}
if fi, _ := os.Stat(path("sync.json")); runtime.GOOS != "windows" && fi.Mode().Perm() != 0o600 { // Windows has no such bits
t.Fatalf("sync.json is %v", fi.Mode())
}
// the first: its setup goes up, sealed
if v := now(t); v.Error != "" || v.Last.IsZero() || v.Notice != nil {
t.Fatalf("first: %+v", v)
}
up := fake.files["/dav/magpie/magpie.magpie-backup"]
if len(up) == 0 || strings.Contains(string(up), "deepseek") || strings.Contains(string(up), `"providers"`) {
t.Fatalf("on the server: %q", up)
}
// nothing changed: nothing written
now(t)
if fake.puts != 1 {
t.Fatalf("%d puts", fake.puts)
}
// b joins: a's setup comes in, b's own proxy and window stay, and what
// b had is kept aside
b.use(t)
provider.Save(provider.Provider{ID: "mine", Name: "Mine", Chat: "https://x/v1", Key: "kb"})
settings.Save(settings.Settings{Proxy: "direct", Window: []int{1, 2}})
Configure(Config{URL: cfg.URL, User: "me", Password: "pw", Passphrase: "correct horse", Keys: true, Agents: true})
v := now(t)
if v.Notice == nil || !slices.Contains(v.Notice.Here, "providers") || !slices.Contains(v.Notice.Here, "settings") {
t.Fatalf("joined: %+v", v)
}
if got := ids(); !slices.Equal(got, []string{"deepseek=k1"}) {
t.Fatalf("b's providers: %v", got)
}
if s := settings.Load(); s.Theme != "dark" || s.Proxy != "direct" || !slices.Equal(s.Window, []int{1, 2}) {
t.Fatalf("b's settings: %+v", s)
}
// nor does a's menu bar: what shows beside the icon is b's own (yoooo
// on Discord)
if s := settings.Load(); len(s.TrayUsages) != 0 || s.TrayUsage != "" || s.TrayUsageEvery == 5 || s.TrayNoLogos {
t.Fatalf("b's menu bar: %+v", s)
}
if ps, _ := profile.Load(); len(ps) != 1 {
t.Fatalf("b's profiles: %v", ps)
}
kept, _ := filepath.Glob(filepath.Join(settings.Dir(), "sync", "*-this-computer"+backup.Ext))
if len(kept) != 1 {
t.Fatalf("kept: %v", kept)
}
if old, err := backup.Open(must(os.ReadFile(kept[0])), "correct horse"); err != nil || old.Providers[0].ID != "mine" {
t.Fatalf("kept copy: %v %+v", err, old.Providers)
}
Dismiss()
puts := fake.puts
now(t)
if fake.puts != puts || Status().Notice != nil {
t.Fatalf("b pushed back what it brought in (%d puts)", fake.puts-puts)
}
// a adds one, b removes one: each reaches the other
a.use(t)
provider.Save(provider.Provider{ID: "kimi", Name: "Kimi", Chat: "https://api.moonshot.cn/v1", Key: "k2"})
now(t)
b.use(t)
now(t)
if got := ids(); !slices.Equal(got, []string{"deepseek=k1", "kimi=k2"}) {
t.Fatalf("b after a added: %v", got)
}
provider.Delete("deepseek")
profile.Delete("work")
now(t)
a.use(t)
if v := now(t); v.Notice != nil {
t.Fatalf("a: %+v", v.Notice)
}
if got := ids(); !slices.Equal(got, []string{"kimi=k2"}) {
t.Fatalf("a after b removed: %v", got)
}
if ps, _ := profile.Load(); len(ps) != 0 {
t.Fatalf("a's profiles: %v", ps)
}
if s := settings.Load(); s.Proxy != "http://127.0.0.1:7890" {
t.Fatalf("a's proxy: %q", s.Proxy)
}
// both change the settings: the older change gives way, and is kept
settings.Save(settings.Settings{Theme: "light", Proxy: "http://127.0.0.1:7890"})
now(t) // a's is on the server now
b.use(t)
settings.Save(settings.Settings{Theme: "system", Lang: "zh", Proxy: "direct"})
old := time.Now().Add(-time.Hour)
os.Chtimes(settings.Path(), old, old)
v = now(t)
if v.Notice == nil || !slices.Equal(v.Notice.Here, []string{"settings"}) || len(v.Notice.There) != 0 {
t.Fatalf("older here: %+v", v.Notice)
}
if s := settings.Load(); s.Theme != "light" || s.Lang == "zh" {
t.Fatalf("b's settings: %+v", s)
}
// …and the newer one stays, the server's kept aside
a.use(t)
settings.Save(settings.Settings{Theme: "dark", Proxy: "http://127.0.0.1:7890"})
os.Chtimes(settings.Path(), old, old)
now(t)
b.use(t)
Dismiss()
settings.Save(settings.Settings{Theme: "light", Lang: "en", Proxy: "direct"})
v = now(t)
if v.Notice == nil || !slices.Equal(v.Notice.There, []string{"settings"}) {
t.Fatalf("newer here: %+v", v.Notice)
}
a.use(t)
now(t)
if s := settings.Load(); s.Lang != "en" {
t.Fatalf("a didn't get b's newer settings: %+v", s)
}
// a computer that sends no keys leaves the server's where they are
c := newComputer(t)
c.use(t)
Configure(Config{URL: cfg.URL, User: "me", Password: "pw", Passphrase: "correct horse", Keys: false, Agents: true})
now(t)
provider.Save(provider.Provider{ID: "kimi", Name: "Kimi 2", Chat: "https://api.moonshot.cn/v1", Key: "k2"})
now(t)
a.use(t)
now(t)
if ps, _ := provider.Stored(); len(ps) != 1 || ps[0].Name != "Kimi 2" || ps[0].Key != "k2" {
t.Fatalf("a after c renamed: %+v", ps)
}
remote, _ := backup.Open(fake.files["/dav/magpie/magpie.magpie-backup"], "correct horse")
if remote.Providers[0].Key != "k2" {
t.Fatalf("the server lost the key: %+v", remote.Providers)
}
// the wrong passphrase, the wrong password
d := newComputer(t)
d.use(t)
Configure(Config{URL: cfg.URL, User: "me", Password: "pw", Passphrase: "other"})
if err := Now(ctx); err == nil || !strings.Contains(err.Error(), "passphrase") || Status().Error == "" {
t.Fatalf("wrong passphrase: %v", err)
}
Configure(Config{URL: cfg.URL, User: "me", Password: "nope", Passphrase: "correct horse"})
if err := Now(ctx); err == nil || !strings.Contains(err.Error(), "password") {
t.Fatalf("wrong password: %v", err)
}
// a password or passphrase left empty is kept
Configure(Config{URL: cfg.URL, User: "me"})
if c, _ := Load(); c.Password != "nope" || c.Passphrase != "correct horse" {
t.Fatalf("kept: %+v", c)
}
if err := Off(); err != nil || Status().On {
t.Fatal("still on")
}
}
// The library goes as a part of its own: a's instructions, servers and
// skills reach b and are written into b's agents, b pushes nothing back,
// a change on either side reaches the other, and a computer that leaves
// the library out neither sends nor takes it.
// A server that answers a read in a folder not made yet with a 409 (坚果云,
// #114): no backup there yet, so the first sync makes the folder and puts it.
func TestSyncNutstore(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true}, nutstore: true}
srv := httptest.NewServer(fake)
defer srv.Close()
newComputer(t).use(t)
provider.Save(provider.Provider{ID: "deepseek", Name: "DeepSeek", Chat: "https://api.deepseek.com/v1", Key: "k1"})
if err := Configure(Config{URL: srv.URL + "/dav/", User: "me", Password: "pw", Passphrase: "correct horse", Keys: true}); err != nil {
t.Fatal(err)
}
if err := Now(context.Background()); err != nil {
t.Fatal(err)
}
if v := Status(); v.Error != "" || len(fake.files["/dav/magpie/magpie.magpie-backup"]) == 0 || !fake.dirs["/dav/magpie"] {
t.Fatalf("first sync: %+v", v)
}
}
// The order the providers were put in on the Providers tab (#499) goes
// with them (ARNO on Discord: 发现webdav同步的时候没有同步provider的顺序):
// b joins and lists them as a does; b arranges them again and a follows;
// a moving one alone is a change that syncs.
func TestSyncProviderOrder(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true}}
srv := httptest.NewServer(fake)
defer srv.Close()
cfg := Config{URL: srv.URL + "/dav/", User: "me", Password: "pw", Passphrase: "correct horse", Keys: true}
now := func(t *testing.T) {
t.Helper()
if err := Now(context.Background()); err != nil {
t.Fatal(err)
}
}
listed := func() []string {
var out []string
for _, p := range provider.All() {
out = append(out, p.ID)
}
return out
}
a, b := newComputer(t), newComputer(t)
a.use(t)
for _, id := range []string{"one", "two", "three"} {
provider.Save(provider.Provider{ID: id, Name: id, Chat: "https://" + id + ".example.com/v1", Key: "k-" + id})
}
if err := provider.SetOrder([]string{"three", "one", "two"}); err != nil {
t.Fatal(err)
}
Configure(cfg)
now(t)
b.use(t)
Configure(cfg)
now(t)
if got := listed(); !slices.Equal(got, []string{"three", "one", "two"}) {
t.Fatalf("b after joining: %v", got)
}
puts := fake.puts
now(t)
if fake.puts != puts {
t.Fatal("b pushed back the order it brought in")
}
// only the order changes on b: a gets it
if err := provider.SetOrder([]string{"two", "three", "one"}); err != nil {
t.Fatal(err)
}
now(t)
if fake.puts != puts+1 {
t.Fatalf("b's new order not pushed (%d puts)", fake.puts-puts)
}
a.use(t)
now(t)
if got := listed(); !slices.Equal(got, []string{"two", "three", "one"}) {
t.Fatalf("a after b arranged: %v", got)
}
}
func TestSyncLibrary(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true}}
srv := httptest.NewServer(fake)
defer srv.Close()
ctx := context.Background()
// a setup from before the library could be left out: it goes
cfg := Config{URL: srv.URL + "/dav/", User: "me", Password: "pw", Passphrase: "correct horse", Keys: true, Agents: true}
now := func(t *testing.T) View {
t.Helper()
if err := Now(ctx); err != nil {
t.Fatal(err)
}
return Status()
}
shared := func() string {
iv, _ := library.ReadInstructions()
return iv.Shared
}
say := func(t *testing.T, text string) {
t.Helper()
if _, err := library.SaveInstructions(library.InstructionsChange{Shared: &text, Agents: []string{"claude"}}); err != nil {
t.Fatal(err)
}
}
a, b := newComputer(t), newComputer(t)
a.use(t)
os.MkdirAll(filepath.Join(string(a), ".claude"), 0o755)
say(t, "Be brief.")
dir := filepath.Join(string(a), "src", "notes")
os.MkdirAll(dir, 0o755)
os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("---\nname: notes\ndescription: Notes\n---\n"), 0o644)
if _, err := library.InstallSkills(dir, []string{""}, []string{"claude"}); err != nil {
t.Fatal(err)
}
Configure(cfg)
if v := now(t); v.Error != "" || !v.Library {
t.Fatalf("a: %+v", v)
}
b.use(t)
os.MkdirAll(filepath.Join(string(b), ".claude"), 0o755)
Configure(cfg)
if v := now(t); v.Error != "" {
t.Fatalf("b: %+v", v)
}
if got := shared(); got != "Be brief." {
t.Fatalf("b's instructions: %q", got)
}
if s, _ := os.ReadFile(filepath.Join(string(b), ".claude", "CLAUDE.md")); !strings.Contains(string(s), "Be brief.") {
t.Fatalf("b's claude wasn't given them: %q", s)
}
if _, err := os.Stat(filepath.Join(string(b), ".claude", "skills", "notes", "SKILL.md")); err != nil {
t.Fatalf("b's claude has no notes: %v", err)
}
puts := fake.puts
now(t)
now(t)
if fake.puts != puts {
t.Fatalf("b pushed back the library it brought in (%d puts)", fake.puts-puts)
}
// b changes it: a gets it
say(t, "Be thorough.")
now(t)
a.use(t)
now(t)
if got := shared(); got != "Be thorough." {
t.Fatalf("a after b changed: %q", got)
}
puts = fake.puts
now(t)
if fake.puts != puts {
t.Fatal("a pushed again")
}
// a computer that leaves the library out keeps its own
c := newComputer(t)
c.use(t)
off := false
lc := cfg
lc.Library = &off
Configure(lc)
say(t, "Mine alone.")
if v := now(t); v.Library || v.Error != "" {
t.Fatalf("c: %+v", v)
}
if got := shared(); got != "Mine alone." {
t.Fatalf("c's library was synced: %q", got)
}
remote, _ := backup.Open(fake.files["/dav/magpie/magpie.magpie-backup"], "correct horse")
if remote.Library == nil || remote.Library.Texts["default"] != "Be thorough." {
t.Fatalf("the server's library: %+v", remote.Library)
}
}
// Another computer syncing between this one's read and write: the write
// is refused, and it syncs again over the other's.
func TestSyncRace(t *testing.T) {
fake := &fakeDAV{files: map[string][]byte{}, etags: map[string]string{}, dirs: map[string]bool{"/dav": true, "/dav/magpie": true}}
var once sync.Once
var raced []byte
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPut && fake.files["/dav/magpie/magpie.magpie-backup"] != nil {
once.Do(func() { // someone else's write lands first
fake.mu.Lock()
fake.n++
fake.files[r.URL.Path], fake.etags[r.URL.Path] = raced, fmt.Sprintf(`"%d"`, fake.n)
fake.mu.Unlock()
})
}
fake.ServeHTTP(w, r)
}))
defer srv.Close()
cfg := Config{URL: srv.URL + "/dav", User: "me", Password: "pw", Passphrase: "p", Keys: true}
a := newComputer(t)
a.use(t)
provider.Save(provider.Provider{ID: "one", Name: "One", Chat: "https://x/v1", Key: "k"})
Configure(cfg)
if err := Now(context.Background()); err != nil {
t.Fatal(err)
}
// what another computer would have put: one and two
other, _ := backup.Collect(true, "")
other.Providers = append(other.Providers, provider.Provider{ID: "two", Name: "Two", Chat: "https://y/v1", Key: "k"})
other.Created = time.Now().Add(-time.Minute)
raced, _ = backup.Seal(other, "p")
provider.Save(provider.Provider{ID: "three", Name: "Three", Chat: "https://z/v1", Key: "k"})
if err := Now(context.Background()); err != nil {
t.Fatal(err)
}
// providers changed on both since: the newer, here, stays whole, and
// the other's is kept aside
if got := ids(); !slices.Equal(got, []string{"one=k", "three=k"}) {
t.Fatalf("after the race: %v", got)
}
if n := Status().Notice; n == nil || !slices.Equal(n.There, []string{"providers"}) {
t.Fatalf("notice: %+v", n)
}
remote, err := backup.Open(fake.files["/dav/magpie/magpie.magpie-backup"], "p")
if err != nil || !slices.ContainsFunc(remote.Providers, func(p provider.Provider) bool { return p.ID == "three" }) {
t.Fatalf("server: %v %+v", err, remote.Providers)
}
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
// The password saved goes with the server and user it was given for: left
// empty, it is kept for another folder there; for another server or user
// it is asked for, never sent to them, and a server with no user needs none
// once the user is taken away. SavedPassword says so before Configure does.
func TestConfigurePassword(t *testing.T) {
newComputer(t).use(t)
withUser := Config{URL: "https://dav.example.com/dav/", User: "me", Password: "pw", Passphrase: "correct horse"}
tokenOnly := Config{URL: "https://dav.example.com/dav/", Password: "token", Passphrase: "correct horse"}
for _, tc := range []struct {
from Config
url, user string
want string // the password saved after
asked bool
}{
{withUser, "https://dav.example.com/dav/other/", "me", "pw", false},
{withUser, "https://DAV.example.com/dav/", " me ", "pw", false},
{withUser, "https://other.example.com/dav/", "me", "", true},
{withUser, "http://dav.example.com/dav/", "me", "", true}, // not sent where it can be read on the way
{withUser, "https://dav.example.com:8443/dav/", "me", "", true},
{withUser, "https://dav.example.com/dav/", "you", "", true},
{withUser, "https://dav.example.com/dav/", "", "", false}, // a server that asks for no sign-in
{tokenOnly, "https://dav.example.com/dav/other/", "", "token", false},
{tokenOnly, "https://other.example.com/dav/", "", "", true},
} {
if err := Configure(tc.from); err != nil {
t.Fatal(err)
}
c := Config{URL: tc.url, User: tc.user}
if kept, needed := SavedPassword(c); kept != (tc.want != "") || needed != tc.asked {
t.Errorf("%s as %q: SavedPassword kept %v, needed %v", tc.url, tc.user, kept, needed)
}
err := Configure(c)
if tc.asked {
if err == nil || !strings.Contains(err.Error(), "type the password") {
t.Errorf("%s as %q: %v, want the password asked for", tc.url, tc.user, err)
}
if c, _ := Load(); c.URL != tc.from.URL || c.Password != tc.from.Password {
t.Errorf("%s as %q: saved anyway: %+v", tc.url, tc.user, c)
}
continue
}
if err != nil {
t.Fatal(err)
}
if c, _ := Load(); c.Password != tc.want || c.Passphrase != "correct horse" {
t.Errorf("%s as %q: password %q, passphrase %q", tc.url, tc.user, c.Password, c.Passphrase)
}
}
}