package web
import (
"net/url"
"path/filepath"
"regexp"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// newTokenListForge is the account page over a real token store, with dan
// holding check-in on alpha.
func newTokenListForge(t *testing.T) (prefsForge, *oltp.DB) {
t.Helper()
dir := t.TempDir()
ix, err := index.Open(filepath.Join(dir, "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
db, err := oltp.Open(filepath.Join(dir, "oltp.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
if err := db.AddAccount("dan", "pw-dan-12345", false); err != nil {
t.Fatal(err)
}
prefs := newMemPrefs()
r := ops.NewRegistry()
if err := api.RegisterAll(r, api.Deps{Version: "t",
Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix, Code: "c1"}}),
Prefs: prefs,
SyncTokens: db,
}); err != nil {
t.Fatal(err)
}
h := HandlerWith(r, nil, nil, []Option{WithSessions(fakeSessions{user: "dan"}), WithPrefs(prefs)})
return prefsForge{h: h, prefs: prefs}, db
}
var tokenRowRe = regexp.MustCompile(`data-hook="push-row" data-token="([0-9A-Z]+)" data-state="([a-z]+)"`)
// tokenRows is each listed token's id and state, in page order.
func tokenRowsOf(body string) [][2]string {
var out [][2]string
for _, m := range tokenRowRe.FindAllStringSubmatch(body, -1) {
out = append(out, [2]string{m[1], m[2]})
}
return out
}
// The Push credential section is a list: minting adds a row and leaves
// the others; each row revokes on its own; a revoked row stays, marked.
func TestThePushCredentialSectionIsAList(t *testing.T) {
f, db := newTokenListForge(t)
if rec := f.post(t, "/me/push-token", url.Values{}); rec.Code != 200 {
t.Fatalf("FAIL: first mint answered %d", rec.Code)
}
rec := f.post(t, "/me/push-token", url.Values{})
if rec.Code != 200 {
t.Fatalf("FAIL: second mint answered %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), `data-hook="push-token"`) {
t.Errorf("FAIL: the second mint does not show its value")
}
_, body := f.get(t, "/me")
rows := tokenRowsOf(body)
if len(rows) != 2 || rows[0][1] != "live" || rows[1][1] != "live" {
t.Fatalf("FAIL: after two mints the list is %v, want two live rows", rows)
}
t.Logf("ok: two mints, two live rows")
for _, hook := range []string{`data-hook="push-list"`, `data-hook="push-held"`, `data-hook="push-row-revoke"`} {
if !strings.Contains(body, hook) {
t.Errorf("FAIL: the list lacks %s", hook)
}
}
for _, field := range []string{"push-row-name", "push-row-scope", "push-row-created", "push-row-expires", "push-row-used"} {
if strings.Count(body, `data-hook="`+field+`"`) != 2 {
t.Errorf("FAIL: not every row carries %s", field)
}
}
// Revoke the first: it stays listed as revoked, the other stays live
// and still opens the sync door.
first, second := rows[0][0], rows[1][0]
if rec := f.post(t, "/me/push-token/revoke", url.Values{"id": {first}}); rec.Code != 303 {
t.Fatalf("FAIL: revoking one answered %d", rec.Code)
}
_, body = f.get(t, "/me")
states := map[string]string{}
for _, r := range tokenRowsOf(body) {
states[r[0]] = r[1]
}
if states[first] != "revoked" || states[second] != "live" {
t.Errorf("FAIL: after revoking one, states are %v", states)
} else {
t.Logf("ok: revoking one row leaves the other live")
}
if strings.Count(body, `data-hook="push-row-revoke"`) != 1 {
t.Errorf("FAIL: a revoked row still offers revoke")
}
// Legacy rows are listed per repository, with a revoke of their own.
if err := db.SetSyncUser("alpha", "dan", "issued-by-hand"); err != nil {
t.Fatal(err)
}
_, body = f.get(t, "/me")
if !strings.Contains(body, `data-hook="push-legacy" data-repo="alpha"`) {
t.Errorf("FAIL: the hand-issued row is not listed")
}
if rec := f.post(t, "/me/push-token/revoke", url.Values{"repo": {"alpha"}}); rec.Code != 303 {
t.Errorf("FAIL: revoking the legacy row answered %d", rec.Code)
}
if repos, _ := db.LegacySyncUsers("dan"); len(repos) != 0 {
t.Errorf("FAIL: the legacy row survived its revoke")
}
}