fix(account): do not claim the invite was emailed when mail is off
The create notice said "emailed the invitation" even when the mailer is the no-op fallback (no SMTP host). It now reports that email is unconfigured and points at the shown link — the copy/paste fallback the spec calls for — and only claims a send when a real mailer is wired. Covered by a test.
This commit is contained in:
parent
13133717e1
commit
3a258aeb55
2 changed files with 47 additions and 5 deletions
|
|
@ -222,8 +222,17 @@ func (a *Actions) Create(w http.ResponseWriter, r *http.Request, rc *server.Requ
|
||||||
}
|
}
|
||||||
link := a.inviteLink(guid)
|
link := a.inviteLink(guid)
|
||||||
a.sendInvite(ctx, name, email, link)
|
a.sendInvite(ctx, name, email, link)
|
||||||
return a.renderList(w, r, rc, http.StatusOK,
|
return a.renderList(w, r, rc, http.StatusOK, a.inviteNotice(name), "notice", link)
|
||||||
fmt.Sprintf("Created %s and emailed the invitation.", name), "notice", link)
|
}
|
||||||
|
|
||||||
|
// inviteNotice is the outcome line after a successful create. It does not claim
|
||||||
|
// an email was sent when mail is not configured — the link below is the
|
||||||
|
// fallback the admin shares by hand (SPEC-010 §5).
|
||||||
|
func (a *Actions) inviteNotice(name string) string {
|
||||||
|
if _, nop := a.d.Mailer.(mail.Nop); nop || a.d.Mailer == nil {
|
||||||
|
return fmt.Sprintf("Created %s. Email is not configured — share the invitation link below.", name)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Created %s and emailed the invitation.", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createError maps a users.Create sentinel to an HTTP status + a human message.
|
// createError maps a users.Create sentinel to an HTTP status + a human message.
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,19 @@ func spec010Gate() *server.Tiers {
|
||||||
// ceremony the account page reuses) and the admin-only role gate. It returns
|
// ceremony the account page reuses) and the admin-only role gate. It returns
|
||||||
// the harness and the recording mailer.
|
// the harness and the recording mailer.
|
||||||
func spec010Door(t *testing.T) (*harness, *mail.Fake) {
|
func spec010Door(t *testing.T) (*harness, *mail.Fake) {
|
||||||
|
t.Helper()
|
||||||
|
mailer := mail.NewFake()
|
||||||
|
return spec010DoorWith(t, mailer), mailer
|
||||||
|
}
|
||||||
|
|
||||||
|
// spec010DoorWith is spec010Door with an explicit mailer (a test can pass
|
||||||
|
// mail.Nop{} to exercise the "email is not configured" path).
|
||||||
|
func spec010DoorWith(t *testing.T, mailer mail.Mailer) *harness {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
mods, err := module.Load(nil)
|
mods, err := module.Load(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("module.Load: %v", err)
|
t.Fatalf("module.Load: %v", err)
|
||||||
}
|
}
|
||||||
mailer := mail.NewFake()
|
|
||||||
|
|
||||||
h := newHarness(t, func(d *server.Deps) {
|
h := newHarness(t, func(d *server.Deps) {
|
||||||
reg := server.NewMapRegistry()
|
reg := server.NewMapRegistry()
|
||||||
|
|
@ -62,7 +69,7 @@ func spec010Door(t *testing.T) (*harness, *mail.Fake) {
|
||||||
})
|
})
|
||||||
web.SetNav(mods)
|
web.SetNav(mods)
|
||||||
t.Cleanup(func() { web.SetNav(nil) })
|
t.Cleanup(func() { web.SetNav(nil) })
|
||||||
return h, mailer
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec010Team creates an ACTIVE team member under the admin (for the non-admin
|
// spec010Team creates an ACTIVE team member under the admin (for the non-admin
|
||||||
|
|
@ -245,8 +252,32 @@ func TestSpec010AC3_CannotCreateARoleAtOrAboveYourOwn(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AC-4 — the invite path enrols ────────────────────────────────────────────
|
// TestSpec010AC3_NoMailStillShowsTheLink: with mail unconfigured the invite is
|
||||||
|
// not claimed to be emailed — the link is the fallback (SPEC-010 §5).
|
||||||
|
func TestSpec010AC3_NoMailStillShowsTheLink(t *testing.T) {
|
||||||
|
h := spec010DoorWith(t, mail.Nop{})
|
||||||
|
|
||||||
|
body := createUser(t, h, "nop@example.com", "team")
|
||||||
|
u, err := h.users.ByEmail(h.ctx, "nop@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ByEmail: %v", err)
|
||||||
|
}
|
||||||
|
idn, err := h.ids.ByUserID(h.ctx, u.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ByUserID: %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "/"+idn.GUID) {
|
||||||
|
t.Errorf("the invite link is not shown when mail is unconfigured:\n%s", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, "emailed the invitation") {
|
||||||
|
t.Errorf("the page claims an email was sent with no mailer configured:\n%s", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, "not configured") {
|
||||||
|
t.Errorf("the page does not say mail is unconfigured:\n%s", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── the account page's factors ───────────────────────────────────────────────
|
||||||
// TestSpec010_AccountShowsTheUsersFactors: the account page reports the passkey
|
// TestSpec010_AccountShowsTheUsersFactors: the account page reports the passkey
|
||||||
// count and whether TOTP is enrolled (SPEC-010 §4).
|
// count and whether TOTP is enrolled (SPEC-010 §4).
|
||||||
func TestSpec010_AccountShowsTheUsersFactors(t *testing.T) {
|
func TestSpec010_AccountShowsTheUsersFactors(t *testing.T) {
|
||||||
|
|
@ -275,6 +306,8 @@ func TestSpec010_AccountShowsTheUsersFactors(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── AC-4 — the invite path enrols ────────────────────────────────────────────
|
||||||
|
|
||||||
// TestSpec010AC4_InvitedUserEnrolsAtTheGUID: the emailed link opens the
|
// TestSpec010AC4_InvitedUserEnrolsAtTheGUID: the emailed link opens the
|
||||||
// enrolment screen (SPEC-006's invite path — no new flow).
|
// enrolment screen (SPEC-006's invite path — no new flow).
|
||||||
func TestSpec010AC4_InvitedUserEnrolsAtTheGUID(t *testing.T) {
|
func TestSpec010AC4_InvitedUserEnrolsAtTheGUID(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue