package oltp
import (
"errors"
"strings"
"testing"
)
const fmRemote = "https://fossil.example.org/proj"
// The password is sealed at rest, kept when a later change leaves it
// blank, never reaches the settings log, and disabling deletes it. Each
// step is logged with its actor and via.
func TestAFossilMirrorKeepsItsPasswordSealedAndLogsEachChange(t *testing.T) {
db := heldDB(t)
const pw = "hunter2-unique-pw"
if _, err := db.SetFossilMirror("proj", fmRemote, "pusher", "", "alice", ""); !errors.Is(err, ErrMirrorRefused) {
t.Fatalf("FAIL: a first enable without a password: %v", err)
}
m, err := db.SetFossilMirror("proj", fmRemote, "pusher", pw, "alice", "TK1")
if err != nil {
t.Fatal(err)
}
if m.Remote != fmRemote || m.User != "pusher" {
t.Errorf("FAIL: the mirror is %+v", m)
}
var sealed string
if err := db.db.QueryRow(`SELECT pass_sealed FROM fossil_mirror WHERE repo = 'proj'`).Scan(&sealed); err != nil {
t.Fatal(err)
}
if sealed == "" || strings.Contains(sealed, pw) {
t.Fatal("FAIL: the password is stored readable")
}
if got, err := db.FossilMirrorPassword("proj", m.Gen); err != nil || got != pw {
t.Fatalf("FAIL: the sealed password does not open: %v", err)
}
// Blank keeps it.
if _, err := db.SetFossilMirror("proj", fmRemote, "pusher", "", "alice", ""); err != nil {
t.Fatalf("FAIL: a blank password on an unchanged mirror: %v", err)
}
cur, _, _ := db.FossilMirrorOf("proj")
if got, _ := db.FossilMirrorPassword("proj", cur.Gen); got != pw {
t.Error("FAIL: a blank password replaced the stored one")
}
// A new destination needs its own.
if _, err := db.SetFossilMirror("proj", "https://other.example.org/proj", "pusher", "", "alice", ""); !errors.Is(err, ErrMirrorRefused) {
t.Errorf("FAIL: a new remote kept the old password: %v", err)
}
if _, err := db.SetFossilMirror("proj", fmRemote, "someone", "", "alice", ""); !errors.Is(err, ErrMirrorRefused) {
t.Errorf("FAIL: a new user kept the old password: %v", err)
}
const pw2 = "second-unique-pw"
if _, err := db.SetFossilMirror("proj", fmRemote, "pusher", pw2, "alice", ""); err != nil {
t.Fatal(err)
}
cur, _, _ = db.FossilMirrorOf("proj")
if got, _ := db.FossilMirrorPassword("proj", cur.Gen); got != pw2 {
t.Error("FAIL: a new password was not kept")
}
if on, err := db.DisableFossilMirror("proj", "bob", ""); err != nil || !on {
t.Fatalf("FAIL: disable: %v %v", on, err)
}
if _, ok, _ := db.FossilMirrorOf("proj"); ok {
t.Error("FAIL: the mirror outlived its disable")
}
var left int
db.db.QueryRow(`SELECT count(*) FROM fossil_mirror`).Scan(&left)
if left != 0 {
t.Error("FAIL: the password outlived the disable")
}
if on, err := db.DisableFossilMirror("proj", "bob", ""); err != nil || on {
t.Errorf("FAIL: disabling nothing: %v %v", on, err)
}
log, err := db.SettingsLog("proj", 20)
if err != nil {
t.Fatal(err)
}
got := map[string]int{}
for _, c := range log {
for _, v := range []string{c.Was, c.Now} {
if strings.Contains(v, pw) || strings.Contains(v, pw2) {
t.Fatalf("FAIL: the password is in the settings log: %+v", c)
}
}
got[c.Field+" "+c.Was+">"+c.Now+" "+c.Actor+" "+c.Via]++
}
for _, want := range []string{
"fossil-mirror off>on alice TK1",
"fossil-mirror-remote >" + fmRemote + " alice TK1",
"fossil-mirror-user >pusher alice TK1",
"fossil-mirror-password set>changed alice ",
"fossil-mirror on>off bob ",
} {
if got[want] != 1 {
t.Errorf("FAIL: settings log lacks %q (has %v)", want, got)
}
}
if len(log) != 5 {
t.Errorf("FAIL: %d log rows, want 5", len(log))
}
t.Log("ok")
}
// A private repository and a taken-down one cannot be mirrored.
func TestAFossilMirrorIsRefusedWherePublishingIsNot(t *testing.T) {
db := heldDB(t)
if err := db.AddRepo("secret", t.TempDir(), "", Private); err != nil {
t.Fatal(err)
}
if _, err := db.SetFossilMirror("secret", fmRemote, "u", "p", "alice", ""); !errors.Is(err, ErrMirrorRefused) {
t.Errorf("FAIL: private: %v", err)
}
if err := db.SetTakedown(Takedown{Repo: "proj", Strength: Disabled, Category: "copyright", Actor: "dmestas"}); err != nil {
t.Fatal(err)
}
if _, err := db.SetFossilMirror("proj", fmRemote, "u", "p", "alice", ""); !errors.Is(err, ErrHeld) {
t.Errorf("FAIL: taken down: %v", err)
}
var n int
db.db.QueryRow(`SELECT count(*) FROM fossil_mirror`).Scan(&n)
if n != 0 {
t.Errorf("FAIL: %d mirrors kept after refusals", n)
}
}
func TestOnlyAnHTTPSRemoteWithNothingInItButAHostAndPathIsKept(t *testing.T) {
for _, ok := range []string{
"https://fossil.craftdesign.group/quarry",
"https://fossil.example.org:8443/a/b",
"https://fossil.example.org/",
// The forge's one outbound rule lets http reach this machine.
"http://127.0.0.1:8080/proj",
} {
if err := CheckFossilRemote(ok); err != nil {
t.Errorf("FAIL: %q refused: %v", ok, err)
}
}
for _, bad := range []string{
"", "http://fossil.example.org/x", "http://10.0.0.5/x", "ssh://[email protected]/x", "fossil.example.org/x",
"https://[email protected]/x", "https://user:[email protected]/x",
"https://fossil.example.org/x?a=b", "https://fossil.example.org/x#f", "https://",
"https://fossil.example.org:0/x", "https://fossil.example.org:99999/x",
"https://fossil.example.org/x y", "https://-oProxy.example.org/x", "-https://fossil.example.org", "file:///tmp/x",
"https://" + strings.Repeat("a", 300) + ".example.org/x",
} {
if err := CheckFossilRemote(bad); !errors.Is(err, ErrMirrorRefused) {
t.Errorf("FAIL: %q kept: %v", bad, err)
}
}
for _, bad := range []string{"", "-x", "a b", "a:b", strings.Repeat("a", 65)} {
if err := checkFossilUser(bad); !errors.Is(err, ErrMirrorRefused) {
t.Errorf("FAIL: user %q kept: %v", bad, err)
}
}
t.Log("ok")
}
// A push carries the generation it read. After a password-only change,
// a remote change, or the mirror being turned off and on, its password
// read is refused and its outcome, success or failure, is not recorded.
func TestAFossilPushOfAnOldConfigurationReadsAndRecordsNothing(t *testing.T) {
changes := map[string]func(db *DB) error{
"a password-only change": func(db *DB) error {
_, err := db.SetFossilMirror("proj", fmRemote, "u", "new-pw", "alice", "")
return err
},
"a remote change": func(db *DB) error {
_, err := db.SetFossilMirror("proj", "https://other.example.org/proj", "u", "new-pw", "alice", "")
return err
},
"a user change": func(db *DB) error {
_, err := db.SetFossilMirror("proj", fmRemote, "v", "new-pw", "alice", "")
return err
},
"off and on again": func(db *DB) error {
if _, err := db.DisableFossilMirror("proj", "alice", ""); err != nil {
return err
}
_, err := db.SetFossilMirror("proj", fmRemote, "u", "old-pw", "alice", "")
return err
},
}
for name, change := range changes {
db := heldDB(t)
old, err := db.SetFossilMirror("proj", fmRemote, "u", "old-pw", "alice", "")
if err != nil {
t.Fatal(err)
}
if got, err := db.FossilMirrorPassword("proj", old.Gen); err != nil || got != "old-pw" {
t.Fatalf("FAIL: %s: the current generation's password: %q %v", name, got, err)
}
if err := change(db); err != nil {
t.Fatal(err)
}
cur, _, _ := db.FossilMirrorOf("proj")
if cur.Gen == old.Gen {
t.Fatalf("FAIL: %s kept the generation", name)
}
if _, err := db.FossilMirrorPassword("proj", old.Gen); !errors.Is(err, ErrFossilMirrorChanged) {
t.Errorf("FAIL: %s: a stale password read: %v", name, err)
}
if err := db.RecordFossilMirrorPush("proj", old.Gen, "abc"); err != nil {
t.Fatal(err)
}
if err := db.RecordFossilMirrorFailure("proj", old.Gen, "no"); err != nil {
t.Fatal(err)
}
if m, _, _ := db.FossilMirrorOf("proj"); m.PushedAt != "" || m.Err != "" || m.Fails != 0 {
t.Errorf("FAIL: %s: a stale outcome landed: %+v", name, m)
}
if _, err := db.FossilMirrorPassword("proj", cur.Gen); err != nil {
t.Errorf("FAIL: %s: the new generation's read: %v", name, err)
}
if err := db.RecordFossilMirrorFailure("proj", cur.Gen, "no"); err != nil {
t.Fatal(err)
}
if m, _, _ := db.FossilMirrorOf("proj"); m.Err != "no" || m.Fails != 1 {
t.Errorf("FAIL: %s: its own failure did not land: %+v", name, m)
}
if err := db.RecordFossilMirrorPush("proj", cur.Gen, "abc"); err != nil {
t.Fatal(err)
}
if m, _, _ := db.FossilMirrorOf("proj"); m.Pushed != "abc" || m.Err != "" || m.Fails != 0 {
t.Errorf("FAIL: %s: its own success did not land: %+v", name, m)
}
}
// A save that changes nothing keeps the generation.
db := heldDB(t)
a, _ := db.SetFossilMirror("proj", fmRemote, "u", "pw", "alice", "")
b, _ := db.SetFossilMirror("proj", fmRemote, "u", "", "alice", "")
if a.Gen != b.Gen {
t.Error("FAIL: an unchanged save bumped the generation")
}
t.Log("ok")
}