Artifact 2652292496bb5f9323ead273f0046f5df20179f7ea916b49b2fbe355a57728cf:
- File
internal/sync/login_realfossil_client_test.go
— part of check-in
[b45ccf4351]
at
2026-10-12 22:52:57
on branch main
— fix(sync): take a uvfile on y alone, without a push card (#296) (#354)
fossil's page_xfer accepts a uvfile on the unversioned-write capability (WrUnver, y) alone; `fossil uv sync` sends uvfile cards with no push card. The handler refused each with "rejected: no push card", so pushing unversioned files from fossil to a go-libfossil server stored nothing.
This is an access change: a requester with y and no Write may now store unversioned files, as in fossil. A refusal (no y, or a HASH that does not match the content) now resets the reply, answers the error and breaks out of the card loop, as xfer_accept_unversioned_file's caller does. Tests pin both sides of the gate, and an interop test pushes with fossil uv sync. The real-fossil test helper now points HOME and XDG_CONFIG_HOME, not only FOSSIL_HOME, at the test's directory. (user: [email protected], size: 4502) [annotate] [blame] [check-ins using]
package sync_test import ( "context" "net/url" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/danmestas/go-libfossil/internal/auth" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/simio" "github.com/danmestas/go-libfossil/testutil" ) // TestRealFossilClientLogsIntoServer checks the server's login handling // against the real fossil client. The server checks that each login card's // nonce is the SHA-1 of the body after the login line, so it must hash // exactly the bytes fossil's http_build_login_card hashed: a pull and a push // under the right password succeed, and the wrong password is refused with // fossil's own "login failed". func TestRealFossilClientLogsIntoServer(t *testing.T) { bin := testutil.RequireFossilBin(t) dir := t.TempDir() srv, err := repo.Create(filepath.Join(dir, "server.fossil"), "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("repo.Create: %v", err) } defer srv.Close() if _, _, err := manifest.Checkin(srv, manifest.CheckinOpts{ Comment: "seed", User: "testuser", Parent: libfossil.FslID(0), Files: []manifest.File{{Name: "seed.txt", Content: []byte("seed\n")}}, }); err != nil { t.Fatalf("Checkin: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() base := serveRepo(ctx, t, srv) // Clone while nobody may still clone, then lock nobody out: from here on // a login card is the only way in. clonePath := filepath.Join(dir, "clone.fossil") fossilOK(t, bin, dir, "clone", base, clonePath) var pc string if err := srv.DB().QueryRow( "SELECT value FROM config WHERE name='project-code'").Scan(&pc); err != nil { t.Fatalf("project-code: %v", err) } if err := auth.CreateUser(srv.DB(), pc, "alice", "pw123", "oi"); err != nil { t.Fatalf("CreateUser: %v", err) } if _, err := srv.DB().Exec("UPDATE user SET cap='' WHERE login='nobody'"); err != nil { t.Fatalf("lock out nobody: %v", err) } withCreds := func(password string) string { u, err := url.Parse(base) if err != nil { t.Fatalf("parse %s: %v", base, err) } u.User = url.UserPassword("alice", password) return u.String() } // fossil reports a server's error card on its output and still exits 0. out, _ := fossilRun(bin, dir, "pull", withCreds("wrong"), "-R", clonePath, "--once") if !strings.Contains(out, "login failed") { t.Fatalf("pull with the wrong password was not refused with \"login failed\":\n%s", out) } fossilOK(t, bin, dir, "pull", withCreds("pw123"), "-R", clonePath, "--once") commitInClone(t, bin, filepath.Join(dir, "work"), clonePath, "alice's change") fossilOK(t, bin, filepath.Join(dir, "work"), "push", withCreds("pw123"), "--once") if _, err := manifest.Crosslink(srv); err != nil { t.Fatalf("Crosslink: %v", err) } var n int if err := srv.DB().QueryRow( "SELECT count(*) FROM event WHERE type='ci' AND comment='alice''s change'").Scan(&n); err != nil { t.Fatalf("count check-ins: %v", err) } if n != 1 { t.Fatalf("server holds %d of alice's check-ins after an authenticated push, want 1", n) } } // commitInClone opens clonePath in work without syncing and commits one new // file there with the given comment. func commitInClone(t *testing.T, bin, work, clonePath, comment string) { t.Helper() if err := os.Mkdir(work, 0o755); err != nil { t.Fatal(err) } fossilOK(t, bin, work, "settings", "autosync", "off", "-R", clonePath) fossilOK(t, bin, work, "open", "--nosync", clonePath) file := filepath.Join(work, "alice.txt") if err := os.WriteFile(file, []byte("from alice\n"), 0o644); err != nil { t.Fatal(err) } fossilOK(t, bin, work, "add", "alice.txt") fossilOK(t, bin, work, "commit", "-m", comment, "--no-warnings") } func fossilRun(bin, dir string, args ...string) (string, error) { cmd := exec.Command(bin, args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "HOME="+dir, "FOSSIL_HOME="+dir, "XDG_CONFIG_HOME="+dir) out, err := cmd.CombinedOutput() return string(out), err } // fossilOK runs fossil and fails the test on a non-zero exit or on an error // card the server sent, which fossil prints as "Error:" without failing. func fossilOK(t *testing.T, bin, dir string, args ...string) { t.Helper() out, err := fossilRun(bin, dir, args...) if err != nil || strings.Contains(out, "Error:") { t.Fatalf("fossil %v: %v\n%s", args, err, out) } }