package api
import (
"context"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// A mention completion must not carry capability letters.
//
// people.mention returned each handle with the account's full capability
// string, "so a composer can say who it is about to name" - and nothing
// ever read it. What it did instead was answer any signed-in account
// with a map of who holds what on the repository. jva measured it as a
// Reader on quarry: nine handles, and the rows for the two site
// administrators carried the whole alphabet,
// abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.
// Anonymous was refused, so the door was "any member", and the letters
// came back on every keystroke of an autocomplete.
//
// Asserted on the source because the property is an ABSENCE. A response
// test can only show that today's fixture has no letters in it; this
// fails if the field comes back at all, which is the thing that must not
// happen again.
func TestAMentionCompletionCarriesNoCapabilityLetters(t *testing.T) {
src, err := os.ReadFile("refs.go")
if err != nil {
t.Fatal(err)
}
decl := string(src)
i := strings.Index(decl, "type mentionHandle struct {")
if i < 0 {
t.Fatal("mentionHandle is gone - if it moved, move this guard with it")
}
body := decl[i:]
if end := strings.Index(body, "}"); end >= 0 {
body = body[:end]
}
if strings.Contains(body, "Caps") || strings.Contains(body, `json:"caps"`) {
t.Errorf("mentionHandle carries capability letters again:\n%s\n"+
"the letters may decide who is completable, but they must not leave the server", body)
}
}
// The filter is the half that has to stay: an account with no letters on
// the repository is not worth completing. Removing the field by removing
// the lookup would quietly start offering strangers.
func TestTheCompletionStillFiltersOnTheLetters(t *testing.T) {
src, err := os.ReadFile("refs.go")
if err != nil {
t.Fatal(err)
}
loc := regexp.MustCompile(`Name:\s+"people.mention"`).FindStringIndex(string(src))
if loc == nil {
t.Fatal("people.mention is gone")
}
body := string(src)[loc[0]:]
if end := strings.Index(body, "\n\tif err := ops.Register"); end > 0 {
body = body[:end]
}
if !strings.Contains(body, "anyCapLetters") {
t.Error("people.mention no longer filters candidates on their letters: " +
"it will complete accounts with nothing on this repository")
}
}
// The @ picker offers this repository's people, never the forge's
// whole account list (#372): on a public repository every signed-in
// account holds the default letters, so the letters alone named
// everyone, searchable by anyone. A contributor and an account granted
// a role here are offered; an account with only the default is not.
func TestAMentionCompletionNamesOnlyTheRepositorysPeople(t *testing.T) {
ix, z, _ := attachStore(t)
db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
for _, who := range []string{"lou", "eve", "stranger"} {
if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
t.Fatal(err)
}
}
if err := db.Grant("eve", "test", oltp.RepoAdmin); err != nil {
t.Fatal(err)
}
d := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: z, Sync: zitSync(z, ix)}}),
Accounts: acctAdapter{db}, Forum: db,
// Every account holds the public default, as on a public repo.
CapsFor: func(string, string) CapSet { return oltp.CapSet("hjor23") }}
r := ops.NewRegistry()
if err := RegisterAll(r, d); err != nil {
t.Fatal(err)
}
if _, err := r.Invoke(as("lou", "3"), "forum.post", forumPostIn{Repo: "test", Title: "hello", Body: "first\n"}); err != nil {
t.Fatal(err)
}
out, err := r.Invoke(ops.WithUser(context.Background(), "stranger"), "people.mention", peopleMentionIn{Repo: "test"})
if err != nil {
t.Fatal(err)
}
got := map[string]bool{}
for _, h := range out.(peopleMentionOut).Handles {
got[h.Handle] = true
}
if !got["lou"] || !got["eve"] || got["stranger"] {
t.Fatalf("FAIL: completion offers %v; want the contributor and the granted account, not a stranger", got)
}
t.Log("ok: the picker names the repository's people, not the forge's accounts")
}