Skip to content
quarry Artifact [755d61270e]
Login

Artifact [755d61270e]

Artifact 755d61270edf872eee4f8cdc7a569ff2ce5c9a149166801992afec4f81106b5b:


package api

import (
	"bytes"
	"context"
	"encoding/json"
	"log"
	"strings"
	"testing"
	"time"

	"craftdesign.group/quarry/internal/ops"
)

// resetToken is what resetAccounts mints, so a test can look for it in
// every place it must not be.
var resetToken = strings.Repeat("ab", 32)

// resetAccounts is fakeAccounts with the invite/reset half of
// AccountAdmin, counting the tokens it mints.
type resetAccounts struct {
	*fakeAccounts
	minted int
}

func (f *resetAccounts) InviteAccount(string, string, bool) (string, error) { return "", nil }
func (f *resetAccounts) InviteFor(string) (string, bool)                    { return "", false }
func (f *resetAccounts) RedeemInvite(string, string) (string, error)        { return "", nil }
func (f *resetAccounts) ResetPassword(string) (string, error) {
	f.minted++
	return resetToken, nil
}
func (f *resetAccounts) TokenKind(string) string                { return "reset" }
func (f *resetAccounts) SetDisabled(string, bool) error         { return nil }
func (f *resetAccounts) Sessions(string) ([]SessionInfo, error) { return nil, nil }
func (f *resetAccounts) EndSession(string, string) error        { return nil }
func (f *resetAccounts) EndSessions(string) error               { return nil }

// textMail records the plain part too: who asked and when is data the
// owner is owed, so the test reads it.
type textMail struct{ sent []struct{ To, Text string } }

func (m *textMail) Send(_ context.Context, to, _, text, _ string) (string, error) {
	m.sent = append(m.sent, struct{ To, Text string }{to, text})
	return "id", nil
}

func resetForge(t *testing.T) (*ops.Registry, *resetAccounts, *textMail, *recordedActs) {
	t.Helper()
	accts := &resetAccounts{fakeAccounts: newFakeAccounts()}
	_ = accts.AddAccount("root", "root-password", true)
	_ = accts.AddAccount("jo", "jos-password", false)
	_ = accts.SetEmail("jo", "[email protected]")
	_ = accts.AddAccount("nomail", "nomails-password", false)
	mail, acts := &textMail{}, &recordedActs{}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail, Acts: acts,
		Origin: "https://forge.example"}); err != nil {
		t.Fatal(err)
	}
	return r, accts, mail, acts
}

// No door lets a site admin choose somebody else's password: the op
// that did is gone, so there is nothing to call.
func TestAnAdminCannotSetAnotherAccountsPassword(t *testing.T) {
	r, accts, _, _ := resetForge(t)
	if _, ok := r.Lookup("account.passwd"); ok {
		t.Fatal("FAIL: account.passwd is still registered")
	}
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.passwd", map[string]any{"name": "jo", "password": "chosen-by-root"}); err == nil {
		t.Fatal("FAIL: account.passwd answered")
	}
	if accts.pw["jo"] != "jos-password" {
		t.Fatal("FAIL: jo's password changed")
	}
	t.Log("ok: no op sets another account's password")
}

// The one thing an admin may do: ask for a link to go to the owner's
// own confirmed address. One email, naming the admin and the time; the
// token is in no response, no log line and no receipt.
func TestAnAdminResetMailsTheOwnerAndHandsBackNoToken(t *testing.T) {
	r, accts, mail, acts := resetForge(t)
	var logged bytes.Buffer
	prev := log.Writer()
	log.SetOutput(&logged)
	defer log.SetOutput(prev)

	before := time.Now().UTC()
	ctx := ops.WithUser(context.Background(), "root")
	out, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "jo"})
	if err != nil {
		t.Fatal(err)
	}
	after := time.Now().UTC()

	if len(mail.sent) != 1 || mail.sent[0].To != "[email protected]" {
		t.Fatalf("FAIL: want one mail to [email protected], got %+v", mail.sent)
	}
	text := mail.sent[0].Text
	if !strings.Contains(text, "root") {
		t.Error("FAIL: the email does not name the admin who asked")
	}
	if !strings.Contains(text, before.Format(resetStamp)) && !strings.Contains(text, after.Format(resetStamp)) {
		t.Error("FAIL: the email does not say when it was asked for")
	}
	// The link is built on the forge's own origin, never one the caller
	// passed: a link to somewhere else would hand the token to whoever
	// runs that somewhere else.
	if !strings.Contains(text, "https://forge.example/invite/"+resetToken) {
		t.Error("FAIL: the link is not on the forge's own origin")
	}

	wire, _ := json.Marshal(out)
	if strings.Contains(string(wire), resetToken) || strings.Contains(string(wire), "/invite/") {
		t.Errorf("FAIL: the response carries the token: %s", wire)
	}
	if strings.Contains(logged.String(), resetToken) {
		t.Error("FAIL: the token reached the log")
	}

	if len(acts.acts) != 1 {
		t.Fatalf("FAIL: want one receipt, got %+v", acts.acts)
	}
	got := acts.acts[0]
	if got.Actor != "root" || got.Op != "account.reset" || got.Owner != "jo" {
		t.Errorf("FAIL: receipt = %+v", got)
	}
	if strings.Contains(got.Actor+got.Op+got.Repo+got.Owner, resetToken) {
		t.Error("FAIL: the token is in the receipt")
	}
	if accts.minted != 1 {
		t.Errorf("FAIL: minted %d tokens", accts.minted)
	}
	t.Log("ok: one mail to the owner, naming root and the time; no token handed back, logged or receipted")
}

// An account with no confirmed address cannot be reset from the forge
// at all — and nothing is minted for it, since a token nobody was sent
// is a token waiting to leak. A pending address is not confirmed.
func TestAnAdminResetRefusesAnAccountWithNoConfirmedAddress(t *testing.T) {
	r, accts, mail, acts := resetForge(t)
	_, _ = accts.SetPendingEmail("nomail", "[email protected]")
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "nomail"}); err == nil {
		t.Fatal("FAIL: a reset was accepted for an account with no confirmed address")
	}
	if accts.minted != 0 || len(mail.sent) != 0 || len(acts.acts) != 0 {
		t.Fatalf("FAIL: refused reset still minted=%d mailed=%d receipted=%d", accts.minted, len(mail.sent), len(acts.acts))
	}
	t.Log("ok: refused, nothing minted, nothing sent")
}

// With no mail plane the link has nowhere to go but back to the admin,
// which is the thing this op no longer does.
func TestAnAdminResetRefusesWithNoMail(t *testing.T) {
	accts := &resetAccounts{fakeAccounts: newFakeAccounts()}
	_ = accts.AddAccount("root", "root-password", true)
	_ = accts.AddAccount("jo", "jos-password", false)
	_ = accts.SetEmail("jo", "[email protected]")
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Origin: "https://forge.example"}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "account.reset", accountResetIn{Name: "jo"}); err == nil {
		t.Fatal("FAIL: a reset was accepted with no way to send it")
	}
	if accts.minted != 0 {
		t.Fatal("FAIL: a token was minted that nobody could be sent")
	}
}

// An admin is still an account: their OWN password changes the way
// everybody's does, given the current one.
func TestAnAdminStillChangesTheirOwnPassword(t *testing.T) {
	r, accts, _, _ := resetForge(t)
	ctx := ops.WithUser(context.Background(), "root")
	if _, err := r.Invoke(ctx, "me.passwd", mePasswdIn{Current: "root-password", New: "a-new-root-password"}); err != nil {
		t.Fatal(err)
	}
	if accts.pw["root"] != "a-new-root-password" {
		t.Fatal("FAIL: root's own password did not change")
	}
	t.Log("ok: an admin changes their own password through me.passwd")
}