package oltp
import (
"strings"
"testing"
)
// The presets are the RFC's most consequential decision, so each letter
// set is asserted, not described.
func TestPresetsAreExactlyWhatTheRFCSays(t *testing.T) {
want := map[string]string{
"Reader": "bcghjoprz2378BC",
"Developer": "uv3bBC78",
"Maintainer": "uv45bBlqCA78",
"Admin": "a",
}
if len(Presets) != len(want) {
t.Fatalf("%d presets, want %d", len(Presets), len(want))
}
for _, p := range Presets {
if p.Caps != want[p.Name] {
t.Errorf("%s = %q, want %q", p.Name, p.Caps, want[p.Name])
}
if PresetOf(p.Caps) != p.Name {
t.Errorf("%s does not read back as itself", p.Name)
}
}
// The write role's gap, closed: a Developer can post and chat.
dev := CapSet(Closure("uv3bBC7"))
if !dev.HasAny("3") || !dev.HasAny("C") || !dev.HasAny("b") {
t.Error("a Developer cannot post, chat, or attach")
}
}
// Fossil's implications: two strings it treats alike compare alike.
func TestClosureFollowsFossilsImplications(t *testing.T) {
for in, want := range map[string]string{
"w": "cnrw",
"5": "2345",
"k": "jkm",
"i": "io",
"6": "23456",
"uv": "uv",
"a b": "ab",
} {
if got := Closure(in); got != want {
t.Errorf("Closure(%q) = %q, want %q", in, got, want)
}
}
}
// A string that is a role plus a little reads as that role plus the
// difference, in sentences — the chip the People table draws.
func TestInferReadsARoleAndItsDifference(t *testing.T) {
rd := Infer("uv3bBC75")
if rd.Role != "Developer" || rd.Exact || len(rd.Plus) == 0 {
t.Fatalf("Developer + moderate read as %+v", rd)
}
if rd.Plus[0].Sentence != "post without moderation" && rd.Plus[len(rd.Plus)-1].Sentence != "moderate the forum" {
t.Errorf("plus = %v", sentences(rd.Plus))
}
rd = Infer("uv3bB78") // Developer minus chat
if rd.Role != "Developer" || len(rd.Minus) != 1 || rd.Minus[0].Sentence != "chat" {
t.Errorf("Developer minus chat read as %+v", rd)
}
if rd := Infer("a"); rd.Role != "Admin" || !rd.Exact {
t.Errorf("a read as %+v", rd)
}
// Far from everything: Custom, not a misleading nearest.
if rd := Infer("iy"); rd.Role != "Custom" {
t.Errorf("a bot's iy read as %q", rd.Role)
}
}
// Compile is the editor: a radio and checkboxes become the string, and
// the string reads back as the same radio and checkboxes.
func TestCompileRoundTripsThroughInfer(t *testing.T) {
s := Compile("Developer", []string{"moderate the forum"}, []string{"chat"})
rd := Infer(s)
if rd.Role != "Developer" || len(rd.Plus) < 1 || len(rd.Minus) != 1 {
t.Fatalf("compiled %q reads as %+v", s, rd)
}
if Compile("Nope", nil, nil) != "" {
t.Error("an unknown role compiled to something")
}
if Compile("Reader", nil, nil) != Closure("bcghjoprz2378BC") {
t.Error("Reader alone does not compile to its preset")
}
}
// Every letter in a string is accounted for in its decoding: nothing a
// person typed is silently dropped from the explanation.
func TestDecodeAccountsForEveryLetter(t *testing.T) {
got := strings.Join(Decode("uv3bBC7xD"), " | ")
for _, want := range []string{"post in the forum", "chat", "see private branches", "reader category", "developer category"} {
if !strings.Contains(got, want) {
t.Errorf("decode lacks %q:\n%s", want, got)
}
}
if got := Decode("Z"); len(got) != 1 || got[0] != "letter Z" {
t.Errorf("an unknown letter vanished: %v", got)
}
}
// The sentence the People table shows, computed from the EFFECTIVE set.
func TestExplainSaysWhatAPersonCanAndCannotDo(t *testing.T) {
db := testDB(t)
db.SetCaps("bob", "alpha", "uv")
got := Explain("bob", db.EffectiveCaps("bob", "alpha"))
if !strings.Contains(got, "check in") || !strings.Contains(got, "but not") || !strings.Contains(got, "post in the forum") {
t.Errorf("the old write role's gap is not stated: %q", got)
}
db.SetCaps("bob", "alpha", Compile("Developer", nil, nil))
got = Explain("bob", db.EffectiveCaps("bob", "alpha"))
if strings.Contains(got, "not post in the forum") || !strings.Contains(got, "chat") {
t.Errorf("a Developer is still described as unable to talk: %q", got)
}
if got := Explain("dan", CapSet("s")); !strings.Contains(got, "site administrator") {
t.Errorf("%q", got)
}
}
// Visibility is a preset over the two category strings, and reads back.
func TestVisibilityPresetsRoundTrip(t *testing.T) {
for _, v := range Visibilities {
if VisibilityOf(v.Nobody, v.Anonymous) != v.Name {
t.Errorf("%s does not read back", v.Name)
}
}
if VisibilityOf(StockNobody, StockAnonymous) != "Custom" {
t.Error("fossil's stock defaults read as a preset; they differ from Public by design")
}
if VisibilityOf("", "") != "Private" {
t.Error("blank categories are not Private")
}
}
func sentences(as []Ability) []string {
out := make([]string, len(as))
for i, a := range as {
out[i] = a.Sentence
}
return out
}
// presetCaps is the preset's string, looked up rather than repeated: a
// test that hard-codes the letters it is checking the meaning of proves
// only that two copies of one string agree.
func presetCaps(t *testing.T, name string) string {
t.Helper()
for _, p := range Presets {
if p.Name == name {
return p.Caps
}
}
t.Fatalf("no preset %q", name)
return ""
}
// Reader must mean what it says ON A REAL REPOSITORY, not just as a
// string. The bug this pins was invisible to every letter-set test
// above, because the letters were right and the EXPANSION was the
// problem: "u" is fossil's reader category, effective() expands it, and
// a repository that has not overridden its categories resolves it
// through fossil's shipped kptw — wiki editing and ticket editing. So
// granting "reads, asks, discusses" handed over two edits, and nothing
// here noticed because nothing here resolved a repository.
func TestReaderCannotEditOnAStockRepository(t *testing.T) {
db := teamDB(t)
if err := db.AddRepo("quarry", "/srv/quarry", "", ""); err != nil {
t.Fatal(err)
}
// The stock categories, exactly as an import or a settings save
// writes them — this is the ordinary state, not a contrived one.
for cat, caps := range map[string]string{
"nobody": StockNobody, "anonymous": StockAnonymous,
"reader": StockReader, "developer": StockDeveloper,
} {
if err := db.SetCategoryCaps("quarry", cat, caps); err != nil {
t.Fatal(err)
}
}
if err := db.SetCaps("ann", "quarry", presetCaps(t, "Reader")); err != nil {
t.Fatal(err)
}
eff := db.EffectiveCaps("ann", "quarry")
// The whole point of the role.
for letter, what := range map[string]string{
"k": "edit wiki pages",
"f": "create wiki pages",
"w": "edit tickets",
"t": "create ticket report formats",
} {
if eff.HasAny(letter) {
t.Errorf("FAIL: a Reader can %s (%s) — the role says reads, asks, discusses", what, letter)
}
}
// `n`, filing a ticket in the store, is NOT asserted against here
// and is not this role's doing: StockAnonymous is "hmnc", so
// fossil grants ticket-filing to a passer-by and every signed-in
// account inherits the anonymous category. Whether quarry should
// keep that is RFC 0017's question about the store tier, not this
// preset's. What the preset must not do is add it itself.
if strings.ContainsRune(presetCaps(t, "Reader"), 'n') {
t.Error("FAIL: the Reader preset now grants store ticket filing of its own")
}
// And it still has to be able to read, discuss, and be signed in.
for letter, what := range map[string]string{
"o": "read the code",
"j": "read the wiki",
"r": "read tickets",
"2": "read the forum",
"3": "post in the forum",
"c": "comment on a ticket",
"C": "chat",
"8": "see CI",
"p": "change their own password, and reach the cross-repository ops quarry gates on p",
} {
if !eff.HasAny(letter) {
t.Errorf("FAIL: a Reader cannot %s (%s)", what, letter)
}
}
}
// The category expansion itself is untouched: `u` still pulls in
// whatever a repository says its readers get. Only the PRESET stopped
// routing through it, so a repository that deliberately widens its
// reader category still reaches the roles that keep `u`.
func TestTheReaderCategoryStillExpandsForWhoeverHoldsU(t *testing.T) {
db := teamDB(t)
if err := db.AddRepo("quarry", "/srv/quarry", "", ""); err != nil {
t.Fatal(err)
}
if err := db.SetCategoryCaps("quarry", "reader", StockReader); err != nil {
t.Fatal(err)
}
if err := db.SetCaps("lou", "quarry", presetCaps(t, "Developer")); err != nil {
t.Fatal(err)
}
if eff := db.EffectiveCaps("lou", "quarry"); !eff.HasAny("k") {
t.Error("FAIL: a Developer's u no longer reaches the reader category")
}
// A hand-written string holding u behaves the same — the preset is
// only where the surprise was most likely to be met.
if err := db.SetCaps("ann", "quarry", "u"); err != nil {
t.Fatal(err)
}
if eff := db.EffectiveCaps("ann", "quarry"); !eff.HasAny("w") {
t.Error("FAIL: a bare u no longer expands")
}
}