package xfer
import (
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"time"
)
// fossilPeer stands up a real fossil server with known users. The
// /user row serialisation is outside the protocol profile, so the only
// honest way to pin it is against fossil itself.
func fossilPeer(t *testing.T) (url, projectCode string) {
t.Helper()
if _, err := exec.LookPath("fossil"); err != nil {
t.Skip("fossil not on PATH")
}
// Not t.TempDir(): fossil server forks per-connection children, and
// a killed parent's stragglers can still be touching the repository
// when the framework's RemoveAll sweeps — "directory not empty",
// the flake quarry's own CI caught on its first week. Own dir, own
// patient cleanup.
dir, err := os.MkdirTemp("", "fossilpeer-")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
for i := 0; i < 40; i++ {
if os.RemoveAll(dir) == nil {
return
}
time.Sleep(50 * time.Millisecond)
}
t.Logf("fossilPeer: %s not fully removed", dir)
})
repo := filepath.Join(dir, "peer.fossil")
run := func(args ...string) string {
t.Helper()
out, err := exec.Command("fossil", args...).CombinedOutput()
if err != nil {
t.Fatalf("fossil %v: %v\n%s", args, err, out)
}
return string(out)
}
run("init", repo, "--admin-user", "root")
run("user", "password", "root", "rootpw", "-R", repo)
run("user", "new", "alice", "Alice Dev", "secretpw", "-R", repo)
run("user", "capabilities", "alice", "ei", "-R", repo)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := ln.Addr().(*net.TCPAddr).Port
ln.Close()
cmd := exec.Command("fossil", "server", repo, "--port", fmt.Sprint(port))
// Its own process group, so the kill reaches the per-connection
// children fossil forks, not only the listener.
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
cmd.Wait()
})
url = fmt.Sprintf("http://127.0.0.1:%d", port)
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if c, err := net.DialTimeout("tcp", fmt.Sprintf("127.0.0.1:%d", port), time.Second); err == nil {
c.Close()
break
}
time.Sleep(50 * time.Millisecond)
}
for _, line := range strings.Split(run("info", repo), "\n") {
if f := strings.Fields(line); len(f) == 2 && f[0] == "project-code:" {
projectCode = f[1]
}
}
if projectCode == "" {
t.Fatal("no project code")
}
return url, projectCode
}
// An admin credential fetches the peer's users, caps verbatim.
func TestFetchUsersFromFossil(t *testing.T) {
url, code := fossilPeer(t)
users, err := FetchUsers(nil, url, code, "root", "rootpw")
if err != nil {
t.Fatalf("fetch: %v", err)
}
got := map[string]RemoteUser{}
for _, u := range users {
got[u.Login] = u
}
if u, ok := got["alice"]; !ok || u.Caps != "ei" {
t.Fatalf("alice's caps did not arrive verbatim: %+v", got)
}
if u, ok := got["root"]; !ok || u.Caps != "s" {
t.Fatalf("root's caps did not arrive: %+v", got)
}
// The credential is fossil's own §6.3 shared secret, so quarry can
// verify a login against it directly (RFC 0004 §6).
if u := got["alice"]; u.PWHash != SharedSecret(code, "alice", "secretpw") {
t.Fatalf("alice's hash is not the §6.3 shared secret: %q", u.PWHash)
}
if got["alice"].Info != "Alice Dev" {
t.Fatalf("info lost: %q", got["alice"].Info)
}
}
// The REMOTE enforces admin — that is what makes this legitimate
// rather than a bypass. A developer gets no user rows at all (fossil
// strips the group from the mask), and a bad password fails the login.
func TestFetchUsersRequiresRemoteAdmin(t *testing.T) {
url, code := fossilPeer(t)
if _, err := FetchUsers(nil, url, code, "alice", "secretpw"); err == nil {
t.Fatal("a non-admin fetched the user table")
}
if _, err := FetchUsers(nil, url, code, "root", "wrong"); err == nil {
t.Fatal("a bad password fetched the user table")
}
}
// The row parser is pinned independently of the network so a format
// drift is a readable failure rather than a mysterious empty import.
func TestParseUserRow(t *testing.T) {
u, err := parseUserRow(`1787417078 'root' pw 'a5df9879' cap 's' info 'it''s mine' jx '{}'`)
if err != nil {
t.Fatal(err)
}
if u.Login != "root" || u.Caps != "s" || u.PWHash != "a5df9879" {
t.Fatalf("row parsed wrong: %+v", u)
}
if u.Info != "it's mine" {
t.Fatalf("doubled quote not undoubled: %q", u.Info)
}
if _, err := parseUserRow("garbage"); err == nil {
t.Fatal("a malformed row parsed clean")
}
}