package web
import (
"net/url"
"path/filepath"
"strings"
"testing"
"craftdesign.group/quarry/internal/api"
"craftdesign.group/quarry/internal/index"
"craftdesign.group/quarry/internal/ops"
)
// A person can get a push credential from a page.
//
// Until this section existed the only door was POST /api/v1/me/sync-token,
// which is not a door a person opens from a browser. That was a gap when
// legacy sync_user rows still worked; it became the whole story when they
// were dropped, because the minted token is now the ONLY credential the
// forge accepts for a push. An account without one cannot push, and no
// page said so or offered to fix it.
//
// The assertions are on data hooks and on the token VALUE, never on the
// prose around them.
func TestAPageMintsAPushCredentialAndShowsItOnce(t *testing.T) {
f, tok := newPushForge(t)
// Before: the page says there is none, and offers to make one.
_, body := f.get(t, "/me")
if !strings.Contains(body, `data-hook="account-push"`) {
t.Fatalf("FAIL: the account page has no push-credential section")
}
if !strings.Contains(body, `data-hook="push-none"`) {
t.Errorf("FAIL: an account with no credential is not told so")
}
if strings.Contains(body, `data-hook="push-token"`) {
t.Errorf("FAIL: a credential was shown before one was made")
}
// Minting renders the value in THAT response.
rec := f.post(t, "/me/push-token", url.Values{})
made := rec.Body.String()
if rec.Code != 200 {
t.Fatalf("FAIL: minting answered %d", rec.Code)
}
if !strings.Contains(made, `data-hook="push-token"`) || !strings.Contains(made, tok.minted) {
t.Fatalf("FAIL: the minted credential is not in the response that made it")
}
if !strings.Contains(made, `data-hook="push-token-once"`) {
t.Errorf("FAIL: nothing says the value is shown once")
}
// And never again. This is the property the whole section turns on:
// the token is readable at the moment it is made and at no other,
// so a page that could show it twice would be storing it somewhere.
_, after := f.get(t, "/me")
if strings.Contains(after, tok.minted) {
t.Errorf("FAIL: the credential is readable again on a later page load")
}
if !strings.Contains(after, `data-hook="push-held"`) {
t.Errorf("FAIL: the page does not say a credential exists")
}
// Revoking puts it back to having none, so the person is told again
// that pushing will be refused.
rev := f.post(t, "/me/push-token/revoke", url.Values{})
if rev.Code != 303 {
t.Errorf("FAIL: revoke answered %d, want a redirect", rev.Code)
}
if tok.held {
t.Errorf("FAIL: revoke did not reach the store")
}
_, gone := f.get(t, "/me")
if !strings.Contains(gone, `data-hook="push-none"`) {
t.Errorf("FAIL: after revoking, the page still claims a credential")
}
}
// A forge with no token store draws no section at all, rather than one
// whose button cannot work — the rule the password control already
// follows on this page.
func TestNoTokenStoreDrawsNoPushSection(t *testing.T) {
f := newPrefsForge(t, nil)
_, body := f.get(t, "/me")
if strings.Contains(body, `data-hook="account-push"`) {
t.Errorf("FAIL: a forge that cannot mint credentials offers to mint one")
}
}
// fakeSyncTokens is the SyncTokens surface, holding one account's token.
type fakeSyncTokens struct {
minted string
held bool
created string
}
func (s *fakeSyncTokens) MintSyncToken(string) (string, error) {
s.held, s.created = true, "2026-09-21T10:00:00Z"
return s.minted, nil
}
func (s *fakeSyncTokens) RevokeSyncToken(string) error { s.held = false; return nil }
func (s *fakeSyncTokens) SyncTokenIssued(string) (string, bool) {
return s.created, s.held
}
func newPushForge(t *testing.T) (prefsForge, *fakeSyncTokens) {
t.Helper()
ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ix.Close() })
prefs := newMemPrefs()
tok := &fakeSyncTokens{minted: "zt_a1b2c3d4e5f60718293a4b5c6d7e8f90"}
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: tok,
}); err != nil {
t.Fatal(err)
}
h := HandlerWith(r, nil, nil, []Option{
WithSessions(fakeSessions{user: "dan"}), WithPrefs(prefs)})
return prefsForge{h: h, prefs: prefs}, tok
}