Skip to content
quarry Artifact [efb854f264]
Login

Artifact [efb854f264]

Artifact efb854f2644b1b30b2116bf060d0abc7b1be210c643efa58e86ee441bef7417e:


package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

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

// fakeMailWeb records who a message went to, never its copy — the
// account-email tests only ever need to know that a send happened and
// to whom.
type fakeMailWeb struct{ sent []string }

func (f *fakeMailWeb) Send(ctx context.Context, to, subject, text, html string) (string, error) {
	f.sent = append(f.sent, to)
	return "id", nil
}

func (f *fakeMailWeb) to(address string) bool {
	for _, a := range f.sent {
		if a == address {
			return true
		}
	}
	return false
}

// emailForge is betaForge plus a mail plane: the account-email surface
// (self-serve and the fleet row) needs both an AccountEmail-capable
// store and something to observe what it sent.
func emailForge(t *testing.T) (*httptest.Server, *betaAccts, *fakeMailWeb) {
	t.Helper()
	accts := newBetaAccts()
	_ = accts.AddAccount("root", "root-pw", true)
	_ = accts.AddAccount("eve", "eve-pw", false)
	sess := &betaSessions{accts: accts, admin: "root"}
	mail := &fakeMailWeb{}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v", Accounts: accts, Mail: mail,
		Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(sess), WithCaps(betaCaps{admin: "root"})},
		Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })))
	t.Cleanup(srv.Close)
	return srv, accts, mail
}

// The section above the matrix (data-hook="account-email") reports
// each of the three states a person's address can be in, and the
// matrix's own email column says it has nowhere to send while there is
// no confirmed address (data-hook="email-unset") rather than offering
// a switch that cannot work.
func TestAccountEmailSectionHooksByState(t *testing.T) {
	srv, accts, _ := emailForge(t)

	page := body(as(t, srv, "eve", "GET", "/me", nil))
	if !strings.Contains(page, `data-hook="account-email"`) {
		t.Fatal("no account-email section")
	}
	if !strings.Contains(page, "No address set.") {
		t.Errorf("the unset state is not reported:\n%s", page)
	}
	if !strings.Contains(page, `data-hook="email-unset"`) {
		t.Error("the matrix's email column does not say it is unset")
	}
	if strings.Contains(page, `data-hook="email-confirm-form"`) {
		t.Error("the code field is drawn with nothing pending")
	}

	res := as(t, srv, "eve", "POST", "/me/email", url.Values{"email": {"[email protected]"}})
	page = body(res)
	if res.StatusCode != http.StatusOK {
		t.Fatalf("setting an address = %d", res.StatusCode)
	}
	if !strings.Contains(page, "Waiting for confirmation of [email protected].") {
		t.Errorf("the pending state is not reported:\n%s", page)
	}
	if !strings.Contains(page, `data-hook="email-unset"`) {
		t.Error("the matrix should still say unset — nothing is confirmed yet")
	}
	if !strings.Contains(page, `data-hook="email-confirm-form"`) {
		t.Error("the code field is not drawn while an address is pending")
	}

	// The confirmation code, as the fake store minted it (the real one
	// only ever leaves the forge inside the mailed message).
	code, ok := accts.emailTokens["eve"]
	if !ok {
		t.Fatalf("no pending confirmation code recorded: %+v", accts.emailTokens)
	}
	res = as(t, srv, "eve", "POST", "/me/email/confirm", url.Values{"code": {code}})
	page = body(res)
	if !strings.Contains(page, "<strong>[email protected]</strong>") || !strings.Contains(page, "Confirmed.") {
		t.Errorf("the confirmed state is not reported:\n%s", page)
	}
	if strings.Contains(page, `data-hook="email-unset"`) {
		t.Error("the matrix still says unset after confirmation")
	}
	if strings.Contains(page, `data-hook="email-confirm-form"`) {
		t.Error("the code field is still drawn with nothing pending")
	}

	a, _ := accts.AccountByName("eve")
	if a.Email != "[email protected]" || a.EmailPending != "" {
		t.Fatalf("account state after confirm = %+v", a)
	}
}

// A wrong code refuses and shows the refusal beside the email form.
func TestAccountEmailConfirmRefusesWrongCode(t *testing.T) {
	srv, _, _ := emailForge(t)
	as(t, srv, "eve", "POST", "/me/email", url.Values{"email": {"[email protected]"}})
	res := as(t, srv, "eve", "POST", "/me/email/confirm", url.Values{"code": {"000000"}})
	page := body(res)
	if !strings.Contains(page, `data-hook="account-email"`) {
		t.Fatal("no account-email section")
	}
	if !strings.Contains(page, "not valid any more") {
		t.Errorf("the wrong code is not refused visibly:\n%s", page)
	}
}

// The fleet People row carries a correction control (data-hook
// "person-email"), and posting it reaches account.email.set — which
// stores the address CONFIRMED at once, unlike the self-serve path.
func TestFleetPeopleRowEmailControl(t *testing.T) {
	srv, accts, mail := emailForge(t)

	page := body(as(t, srv, "root", "GET", "/fleet/people", nil))
	if !strings.Contains(page, `data-hook="person-email"`) {
		t.Fatal("no person-email control on the People row")
	}
	if !strings.Contains(page, `action="/fleet/people/email"`) {
		t.Error("the control does not post to /fleet/people/email")
	}

	res := as(t, srv, "root", "POST", "/fleet/people/email",
		url.Values{"name": {"eve"}, "email": {"[email protected]"}})
	body(res)
	if res.StatusCode != http.StatusSeeOther {
		t.Fatalf("admin set = %d", res.StatusCode)
	}
	a, _ := accts.AccountByName("eve")
	if a.Email != "[email protected]" {
		t.Fatalf("account state after admin set = %+v", a)
	}
	if !mail.to("[email protected]") {
		t.Errorf("the account was not told: %v", mail.sent)
	}
}