package oltp
import (
"errors"
"testing"
)
// A sync applies the categories and keeps everyone else as a list: no
// account is created, and a matching name is only suggested.
func TestFossilUsersAreAReferenceNotAccounts(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("bob", "a long password", false); err != nil {
t.Fatal(err)
}
rows, cats, err := db.ReplaceFossilUsers("alpha", []FossilUser{
{Login: "developer", Caps: "ei"},
{Login: "alice", Caps: "ei", Info: "Alice Dev"},
{Login: "bob", Caps: "k"},
})
if err != nil {
t.Fatal(err)
}
if rows != 2 || cats != 1 {
t.Fatalf("rows %d, categories %d", rows, cats)
}
if got := db.CategoryCaps("alpha", "developer"); got != "ei" {
t.Fatalf("developer category = %q", got)
}
if _, ok := db.AccountByName("alice"); ok {
t.Fatal("a sync created an account")
}
users, err := db.FossilUsers("alpha")
if err != nil {
t.Fatal(err)
}
if len(users) != 2 || users[0].Login != "alice" || users[0].Suggest || !users[1].Suggest {
t.Fatalf("users = %+v", users)
}
if db.EffectiveCaps("bob", "alpha").HasAny("k") {
t.Fatal("a matching name was granted without a link")
}
}
// Only a link grants; a re-sync refreshes a linked account; a login
// that leaves fossil loses its row but not the account's access.
func TestFossilLinkGrantsAndSurvivesResync(t *testing.T) {
db := testDB(t)
if _, _, err := db.ReplaceFossilUsers("alpha", []FossilUser{{Login: "alice", Caps: "i"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "alpha", "alice", "al"); !errors.Is(err, ErrNoAccount) {
t.Fatalf("link to a missing account: %v", err)
}
if err := linkErr(db, "alpha", "nobody-here", ""); !errors.Is(err, ErrNoFossilUser) {
t.Fatalf("link of a missing login: %v", err)
}
if err := db.AddAccount("al", "a long password", false); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "alpha", "alice", "al"); err != nil {
t.Fatal(err)
}
if !db.EffectiveCaps("al", "alpha").HasAny("i") {
t.Fatal("a link did not grant the login's capabilities")
}
if _, _, err := db.ReplaceFossilUsers("alpha", []FossilUser{{Login: "alice", Caps: "k"}}); err != nil {
t.Fatal(err)
}
users, _ := db.FossilUsers("alpha")
if len(users) != 1 || users[0].Linked != "al" || !db.EffectiveCaps("al", "alpha").HasAny("k") {
t.Fatalf("a re-sync lost the link or the refresh: %+v", users)
}
if _, _, err := db.ReplaceFossilUsers("alpha", nil); err != nil {
t.Fatal(err)
}
if users, _ := db.FossilUsers("alpha"); len(users) != 0 {
t.Fatalf("a login gone from fossil kept its row: %+v", users)
}
if !db.EffectiveCaps("al", "alpha").HasAny("k") {
t.Fatal("a sync revoked an account's access without an admin")
}
}
// linkErr links as an actor the guard has nothing to say about here —
// none of these links touches an admin — for the tests about what a
// link does rather than who may make it.
func linkErr(db *DB, repo, login, account string) error {
_, err := db.LinkFossilUser("root", repo, login, account)
return err
}
// Fossil's setup and admin letters are admin here, and an owner's hand
// grant survives the next sync.
func TestFossilSetupLinksAsAdmin(t *testing.T) {
for _, caps := range []string{"s", "a"} {
if got := roleFromCaps(caps); got != RepoAdmin {
t.Errorf("caps %q read as %v, want admin", caps, got)
}
}
db := testDB(t)
if err := db.AddAccount("dan", "a long password", false); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "s"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
t.Fatal(err)
}
g, _ := db.GrantsFor("dan")
if g["zit"] != RepoAdmin {
t.Fatalf("a linked setup user holds %v", g["zit"])
}
}
// A rich string with no a or s is still the link's string, applied
// verbatim; it reads as no preset, and that reading is not a missing grant.
func TestFossilRichCapsApplyOnLink(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("ds", "a long password", false); err != nil {
t.Fatal(err)
}
const rich = "bcfklmnqrvwxyz234567ACD"
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "danshearer", Caps: rich}}); err != nil {
t.Fatal(err)
}
if got, _ := db.CapsOf("ds", "zit"); got != "" {
t.Fatalf("an unlinked row granted %q", got)
}
if err := linkErr(db, "zit", "danshearer", "ds"); err != nil {
t.Fatal(err)
}
if got, _ := db.CapsOf("ds", "zit"); got != rich {
t.Fatalf("link stored %q, want %q", got, rich)
}
}
func TestResyncKeepsAHandGrant(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("dan", "a long password", false); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
t.Fatal(err)
}
if err := db.Grant("dan", "zit", RepoAdmin); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
t.Fatal(err)
}
if got, _ := db.CapsOf("dan", "zit"); got != RepoAdmin.Caps() {
t.Fatalf("a sync downgraded a hand grant to %q", got)
}
// Untouched since the last sync, an account still follows upstream.
if err := db.AddAccount("al", "a long password", false); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "alice", Caps: "i"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "zit", "alice", "al"); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "alice", Caps: "k"}}); err != nil {
t.Fatal(err)
}
if got, _ := db.CapsOf("al", "zit"); got != "k" {
t.Fatalf("an untouched link stopped following: %q", got)
}
}
// A string no preset is near is custom, never "none"; only an empty
// string reads as none.
func TestRichCapsNeverReadAsNone(t *testing.T) {
if got := roleFromCaps("bcfklmnqrvwxyz234567ACD"); got == NoRole || got.String() != "custom" {
t.Fatalf("a rich string reads %q", got)
}
if got := roleFromCaps(""); got != NoRole || got.String() != "none" {
t.Fatalf("an empty string reads %q", got)
}
db := testDB(t)
if err := db.AddAccount("ds", "a long password", false); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("ds", "zit", "bcfklmnqrvwxyz234567ACD"); err != nil {
t.Fatal(err)
}
g, _ := db.GrantsFor("ds")
acct, _ := db.AccountByName("ds")
if g["zit"].String() != "custom" || db.RoleOf(acct, "zit").String() != "custom" {
t.Fatalf("grants %v, role %v", g["zit"], db.RoleOf(acct, "zit"))
}
}
// Setup is the forge account's: a repository string never carries s,
// whether it arrives by link, by hand, or from a row stored before.
func TestRepoSetupNeverPassesSiteAdmin(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("dan", "a long password", false); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "s"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
t.Fatal(err)
}
if got, _ := db.CapsOf("dan", "zit"); got != "a" {
t.Fatalf("link stored %q, want a", got)
}
if db.EffectiveCaps("dan", "zit").HasAny("s") {
t.Fatal("a linked setup user passes a site-admin check")
}
if !db.EffectiveCaps("dan", "zit").HasAny("a") {
t.Fatal("a linked setup user lost admin")
}
// A row stored before the rule: read stripped, then migrated.
if _, err := db.w.Exec(`INSERT INTO repo_caps (account, repo, caps) VALUES ('dan', 'old', 'sg')`); err != nil {
t.Fatal(err)
}
if db.EffectiveCaps("dan", "old").HasAny("s") {
t.Fatal("a stored s passes a site-admin check")
}
if err := db.MigrateSetupCaps(); err != nil {
t.Fatal(err)
}
var raw string
if err := db.db.QueryRow(`SELECT caps FROM repo_caps WHERE account = 'dan' AND repo = 'old'`).Scan(&raw); err != nil || raw != "ga" {
t.Fatalf("migrated to %q (%v)", raw, err)
}
// A site admin keeps s everywhere.
if err := db.AddAccount("root2", "a long password", true); err != nil {
t.Fatal(err)
}
if !db.EffectiveCaps("root2", "zit").HasAny("s") {
t.Fatal("a site admin lost s")
}
}
// An upstream downgrade cannot take a repository's last admin.
func TestSyncKeepsTheLastAdmin(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("dan", "a long password", false); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "a"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
t.Fatal(err)
}
if !db.EffectiveCaps("dan", "zit").HasAny("a") {
t.Fatal("a sync removed the repository's last admin")
}
}
func TestGrantCustomRefuses(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("dan", "a long password", false); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("dan", "zit", "iy"); err != nil {
t.Fatal(err)
}
if err := db.Grant("dan", "zit", Custom); err == nil {
t.Fatal("Grant(Custom) did not refuse")
}
if _, err := db.GrantGuarded("root", "dan", "zit", Custom); err == nil {
t.Fatal("GrantGuarded(Custom) did not refuse")
}
if got, _ := db.CapsOf("dan", "zit"); got != "iy" {
t.Fatalf("a refused grant changed the string to %q", got)
}
}
func TestFossilUsersReportAPinnedLink(t *testing.T) {
db := testDB(t)
if err := db.AddAccount("dan", "a long password", false); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
t.Fatal(err)
}
if err := linkErr(db, "zit", "dmestas", "dan"); err != nil {
t.Fatal(err)
}
if us, _ := db.FossilUsers("zit"); us[0].Pinned {
t.Fatal("a fresh link reads as pinned")
}
if err := db.Grant("dan", "zit", RepoAdmin); err != nil {
t.Fatal(err)
}
if _, _, err := db.ReplaceFossilUsers("zit", []FossilUser{{Login: "dmestas", Caps: "k"}}); err != nil {
t.Fatal(err)
}
us, _ := db.FossilUsers("zit")
if !us[0].Pinned || us[0].Held != RepoAdmin.Caps() || us[0].Caps != "k" {
t.Fatalf("%+v", us[0])
}
}