package api
import (
"context"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// fakeAccounts is the minimal AccountStore + AccountEmail + AccountAuth
// this package's tests need: an in-memory roster, grants, the
// pending/confirm code bookkeeping email.go's ops drive, and a fake
// password verifier for me.passwd.
type fakeAccounts struct {
accounts map[string]Account
grants map[string]map[string]string // account -> repo -> role
pending map[string]string // account -> code
addr map[string]string // account -> pending address
pw map[string]string // account -> current password
ended map[string]bool // account -> EndSessions was called
kept map[string]string // account -> last "keep" SetPasswordSelf saw
grantErr error // when set, Grant fails with this instead of recording
}
func newFakeAccounts() *fakeAccounts {
return &fakeAccounts{
accounts: map[string]Account{},
grants: map[string]map[string]string{},
pending: map[string]string{},
addr: map[string]string{},
pw: map[string]string{},
ended: map[string]bool{},
kept: map[string]string{},
}
}
func (f *fakeAccounts) Accounts() ([]Account, error) {
var out []Account
for _, a := range f.accounts {
out = append(out, a)
}
return out, nil
}
func (f *fakeAccounts) AddAccount(name, password string, admin bool) error {
f.accounts[name] = Account{Name: name, Admin: admin}
f.pw[name] = password
return nil
}
func (f *fakeAccounts) SetPassword(name, password string) error { f.pw[name] = password; return nil }
func (f *fakeAccounts) SetOperator(string, string) error { return nil }
func (f *fakeAccounts) DeleteAccount(name string) error { delete(f.accounts, name); return nil }
func (f *fakeAccounts) Grant(_, account, repo, role string) (string, error) {
if f.grantErr != nil {
return "", f.grantErr
}
if f.grants[account] == nil {
f.grants[account] = map[string]string{}
}
f.grants[account][repo] = role
return "", nil
}
func (f *fakeAccounts) Revoke(_, account, repo string) (string, error) {
delete(f.grants[account], repo)
return "", nil
}
func (f *fakeAccounts) GrantsFor(account string) (map[string]string, error) {
return f.grants[account], nil
}
// The confirmed-address half (AccountEmail).
func (f *fakeAccounts) AccountByName(name string) (Account, bool) {
a, ok := f.accounts[name]
return a, ok
}
func (f *fakeAccounts) SetEmail(name, email string) error {
a := f.accounts[name]
a.Name = name
a.Email = email
f.accounts[name] = a
return nil
}
func (f *fakeAccounts) SetPendingEmail(name, address string) (string, error) {
code := "123456"
f.pending[name] = code
f.addr[name] = address
a := f.accounts[name]
a.Name = name
a.EmailPending = address
f.accounts[name] = a
return code, nil
}
func (f *fakeAccounts) ConfirmEmail(account, code string) (string, bool, error) {
want, ok := f.pending[account]
if !ok || want != code {
return "", false, nil
}
address := f.addr[account]
a := f.accounts[account]
a.Email, a.EmailPending = address, ""
f.accounts[account] = a
delete(f.pending, account)
delete(f.addr, account)
return address, true, nil
}
func (f *fakeAccounts) ClearEmail(name string) error {
a := f.accounts[name]
a.Email, a.EmailPending = "", ""
f.accounts[name] = a
return nil
}
// The self-service credential half (AccountAuth).
func (f *fakeAccounts) VerifyPassword(name, password string) bool {
return f.pw[name] != "" && f.pw[name] == password
}
func (f *fakeAccounts) SetPasswordSelf(name, password, keep string) error {
if _, ok := f.accounts[name]; !ok {
return ops.ErrNotFound
}
f.pw[name] = password
f.ended[name] = true
f.kept[name] = keep
return nil
}
// sentMail is one message fakeMail recorded.
type sentMail struct{ To, Subject, HTML string }
// fakeMail is a MailPlane that records what it was asked to send —
// the recipient and that a send happened, never the copy (RFC 0011
// tests already follow this rule; account mail is no exception).
type fakeMail struct{ sent []sentMail }
func (f *fakeMail) Send(ctx context.Context, to, subject, text, html string) (string, error) {
f.sent = append(f.sent, sentMail{To: to, Subject: subject, HTML: html})
return "id", nil
}
func (f *fakeMail) to(address string) bool {
for _, m := range f.sent {
if m.To == address {
return true
}
}
return false
}
// me.email.set sends the confirmation to the NEW address only — the
// address quarry currently mails (the old, confirmed one, if any)
// hears nothing until the new one is actually confirmed.
func TestEmailSetMailsTheNewAddressNotTheOld(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan", Email: "[email protected]"}
mail := &fakeMail{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
out, err := r.Invoke(ctx, "me.email.set", meEmailSetIn{Email: "[email protected]"})
if err != nil {
t.Fatal(err)
}
set := out.(meEmailSetOut)
if !set.Sent || set.Pending != "[email protected]" {
t.Fatalf("me.email.set = %+v", set)
}
if !mail.to("[email protected]") {
t.Fatalf("the new address was not mailed: %+v", mail.sent)
}
if mail.to("[email protected]") {
t.Fatalf("the old, still-confirmed address was mailed: %+v", mail.sent)
}
a, _ := accts.AccountByName("dan")
if a.EmailPending != "[email protected]" || a.Email != "[email protected]" {
t.Fatalf("account state after set = %+v", a)
}
}
// me.email.confirm promotes the pending address and tells the address
// being LEFT, transactionally, that it is no longer the one in use.
func TestEmailConfirmPromotesAndTellsTheOldAddress(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan", Email: "[email protected]"}
mail := &fakeMail{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(ctx, "me.email.set", meEmailSetIn{Email: "[email protected]"}); err != nil {
t.Fatal(err)
}
// The code itself never leaves the store in this harness (it went
// to the fake mailer, not back to the caller) — read it the way
// ConfirmEmail itself would resolve it, off the fake's own map.
code, ok := accts.pending["dan"]
if !ok {
t.Fatal("no pending code recorded")
}
confirmOut, err := r.Invoke(ctx, "me.email.confirm", meEmailConfirmIn{Code: code})
if err != nil {
t.Fatal(err)
}
if got := confirmOut.(meEmailConfirmOut).Email; got != "[email protected]" {
t.Fatalf("confirm reported %q", got)
}
a, _ := accts.AccountByName("dan")
if a.Email != "[email protected]" || a.EmailPending != "" {
t.Fatalf("account state after confirm = %+v", a)
}
if !mail.to("[email protected]") {
t.Fatalf("the superseded address was not told: %+v", mail.sent)
}
// Confirming again must refuse: the code is spent.
if _, err := r.Invoke(ctx, "me.email.confirm", meEmailConfirmIn{Code: code}); err == nil {
t.Fatal("a spent confirmation code was accepted twice")
}
}
// A code confirmed from a different signed-in account must refuse — it
// can never even be looked at, because ConfirmEmail is always asked
// about the CALLER's own pending code.
func TestEmailConfirmRefusesAnotherAccountsCode(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan"}
accts.accounts["eve"] = Account{Name: "eve"}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
danCtx := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(danCtx, "me.email.set", meEmailSetIn{Email: "[email protected]"}); err != nil {
t.Fatal(err)
}
code := accts.pending["dan"]
eveCtx := ops.WithUser(context.Background(), "eve")
if _, err := r.Invoke(eveCtx, "me.email.confirm", meEmailConfirmIn{Code: code}); err == nil {
t.Fatal("eve confirmed dan's code")
}
// dan's own code must still be live.
if _, err := r.Invoke(danCtx, "me.email.confirm", meEmailConfirmIn{Code: code}); err != nil {
t.Fatalf("dan's own code was burned by eve's attempt: %v", err)
}
}
// me.email.clear blanks both halves.
func TestEmailClear(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan", Email: "[email protected]", EmailPending: "[email protected]"}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(ctx, "me.email.clear", meEmailClearIn{}); err != nil {
t.Fatal(err)
}
a, _ := accts.AccountByName("dan")
if a.Email != "" || a.EmailPending != "" {
t.Fatalf("account state after clear = %+v", a)
}
}
// account.email.set on an account that has a password stores the
// address PENDING, never confirmed, and mails the confirmation code
// there: only the owner, signed in, can make it the address quarry uses.
func TestAccountEmailSetIsAdminAndOnlyPending(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan"}
mail := &fakeMail{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "admin")
if _, err := r.Invoke(ctx, "account.email.set", accountEmailSetIn{Name: "dan", Email: "[email protected]"}); err != nil {
t.Fatal(err)
}
a, _ := accts.AccountByName("dan")
if a.Email != "" || a.EmailPending != "[email protected]" {
t.Fatalf("account state after admin set = %+v", a)
}
if !mail.to("[email protected]") {
t.Fatalf("the code did not go to the new address: %+v", mail.sent)
}
}
// An invitee who has never set a password keeps the direct set: there
// is nothing to sign in to until the invite is accepted.
func TestAccountEmailSetConfirmsAnInviteeAtOnce(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["eve"] = Account{Name: "eve", Pending: true}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: &fakeMail{}}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "admin")
if _, err := r.Invoke(ctx, "account.email.set", accountEmailSetIn{Name: "eve", Email: "[email protected]"}); err != nil {
t.Fatal(err)
}
if a, _ := accts.AccountByName("eve"); a.Email != "[email protected]" {
t.Fatalf("account state after admin set = %+v", a)
}
}
// Without a mail plane, every one of these still answers correctly —
// no error, the write still lands — and simply sends nothing.
func TestNoMailPlaneMeansNoMailAndNoError(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan"}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil { // no Mail
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
out, err := r.Invoke(ctx, "me.email.set", meEmailSetIn{Email: "[email protected]"})
if err != nil {
t.Fatal(err)
}
if out.(meEmailSetOut).Sent {
t.Fatal("Sent = true with no mail plane configured")
}
}
// me.passwd refuses a wrong current password without touching anything.
func TestMePasswdRefusesWrongCurrentPassword(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan"}
accts.pw["dan"] = "correct-horse"
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(ctx, "me.passwd", mePasswdIn{Current: "wrong", New: "new-password"}); err == nil {
t.Fatal("a wrong current password was accepted")
}
if accts.pw["dan"] != "correct-horse" {
t.Fatalf("the password changed despite a wrong current one: %q", accts.pw["dan"])
}
if accts.ended["dan"] {
t.Fatal("sessions were ended despite a refused change")
}
}
// me.passwd, on success, ends every other session (via SetPasswordSelf,
// keeping whatever session id it was given) and mails the account at
// its confirmed address.
func TestMePasswdEndsOtherSessionsAndMailsTheAccount(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan", Email: "[email protected]"}
accts.pw["dan"] = "correct-horse"
mail := &fakeMail{}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
if _, err := r.Invoke(ctx, "me.passwd", mePasswdIn{Current: "correct-horse", New: "new-password", Keep: "abc123"}); err != nil {
t.Fatal(err)
}
if accts.pw["dan"] != "new-password" {
t.Fatalf("the password did not change: %q", accts.pw["dan"])
}
if !accts.ended["dan"] {
t.Fatal("other sessions were not ended")
}
if accts.kept["dan"] != "abc123" {
t.Fatalf("the session to spare was not passed through: %q", accts.kept["dan"])
}
if !mail.to("[email protected]") {
t.Fatalf("the account was not told at its confirmed address: %+v", mail.sent)
}
}
// me.account answers the signed-in account's own identity.
func TestMeAccount(t *testing.T) {
accts := newFakeAccounts()
accts.accounts["dan"] = Account{Name: "dan", Admin: true, Created: "2026-01-01T00:00:00Z"}
r := ops.NewRegistry()
if err := RegisterAll(r, Deps{Version: "v", Accounts: accts}); err != nil {
t.Fatal(err)
}
ctx := ops.WithUser(context.Background(), "dan")
out, err := r.Invoke(ctx, "me.account", meAccountIn{})
if err != nil {
t.Fatal(err)
}
got := out.(meAccountOut)
if got.Name != "dan" || !got.Admin || got.Created != "2026-01-01T00:00:00Z" || !got.PasswordAuth {
t.Fatalf("me.account = %+v", got)
}
}