auto mail generation
This commit is contained in:
168
services/resendInvite_test.go
Normal file
168
services/resendInvite_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"nearle/config"
|
||||
"nearle/models"
|
||||
"nearle/repositories"
|
||||
)
|
||||
|
||||
/*
|
||||
Re-issuing a merchant's first-password link.
|
||||
|
||||
Invitations get lost — spam folders, typo'd addresses, a seven-day expiry that
|
||||
runs out over a holiday. Without a resend the only recovery is a database edit,
|
||||
so this exists.
|
||||
|
||||
It is also the endpoint most at risk of quietly becoming something else. An
|
||||
endpoint that re-issues a working password link for any account IS a password
|
||||
reset, whatever it is called, and nothing on this backend verifies identity well
|
||||
enough to support one. So most of what follows is about what it refuses.
|
||||
*/
|
||||
|
||||
type inviteRepo struct {
|
||||
repositories.TenantRepository
|
||||
target repositories.InviteTarget
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *inviteRepo) PrimaryAdminForTenant(int) (repositories.InviteTarget, error) {
|
||||
if r.err != nil {
|
||||
return repositories.InviteTarget{}, r.err
|
||||
}
|
||||
return r.target, nil
|
||||
}
|
||||
|
||||
type countingInvites struct {
|
||||
calls int
|
||||
sent bool
|
||||
reason string
|
||||
}
|
||||
|
||||
func (c *countingInvites) Invite(_, _ int, _, _ string) (bool, string) {
|
||||
c.calls++
|
||||
return c.sent, c.reason
|
||||
}
|
||||
|
||||
func waiting() repositories.InviteTarget {
|
||||
return repositories.InviteTarget{
|
||||
Userid: 904, Email: "owner@rmart.example", Tenantname: "R Mart", IsSetUp: false,
|
||||
}
|
||||
}
|
||||
|
||||
func TestResendEmailsAMerchantWhoNeverGotOne(t *testing.T) {
|
||||
invites := &countingInvites{sent: true}
|
||||
service := NewTenantService(&inviteRepo{target: waiting()}, invites)
|
||||
|
||||
outcome, err := service.ResendInvite(1147)
|
||||
if err != nil {
|
||||
t.Fatalf("resend: %v", err)
|
||||
}
|
||||
if !outcome.Sent || invites.calls != 1 {
|
||||
t.Fatalf("not sent: %+v, calls=%d", outcome, invites.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResendRefusesAMerchantWhoAlreadyHasAPassword(t *testing.T) {
|
||||
// The line between a resend and a password reset.
|
||||
//
|
||||
// `SetInitialPassword` would refuse such a link anyway, so the merchant
|
||||
// could come to no harm — but the operator would be told mail was sent, the
|
||||
// merchant would follow a link that does nothing, and neither would know
|
||||
// why. Refusing here names the real situation.
|
||||
target := waiting()
|
||||
target.IsSetUp = true
|
||||
|
||||
invites := &countingInvites{sent: true}
|
||||
service := NewTenantService(&inviteRepo{target: target}, invites)
|
||||
|
||||
_, err := service.ResendInvite(1147)
|
||||
if err == nil {
|
||||
t.Fatal("re-invited an account that already has a password")
|
||||
}
|
||||
if invites.calls != 0 {
|
||||
t.Fatal("a link was minted for an account that is already set up")
|
||||
}
|
||||
// Says what to do instead, because the merchant's actual problem is signing
|
||||
// in rather than setting up.
|
||||
if !strings.Contains(err.Error(), "sign-in") {
|
||||
t.Fatalf("the refusal does not say what to do instead: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResendNamesTheBusinessItRefused(t *testing.T) {
|
||||
// An operator working through a list needs to know which one, not that
|
||||
// "an account" was already set up.
|
||||
target := waiting()
|
||||
target.IsSetUp = true
|
||||
|
||||
_, err := NewTenantService(&inviteRepo{target: target}, &countingInvites{}).ResendInvite(1147)
|
||||
if err == nil || !strings.Contains(err.Error(), "R Mart") {
|
||||
t.Fatalf("the refusal does not name the business: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResendPassesThroughALookupFailure(t *testing.T) {
|
||||
// No such tenant, or one whose primary email matches no login — which
|
||||
// happens when the address is changed on the tenant without the account
|
||||
// being changed with it. The fix is to correct one of the two, so the
|
||||
// message has to survive rather than become "could not resend".
|
||||
repo := &inviteRepo{err: errors.New("tenant 1147 has no account matching its primary email address")}
|
||||
invites := &countingInvites{}
|
||||
|
||||
_, err := NewTenantService(repo, invites).ResendInvite(1147)
|
||||
if err == nil || !strings.Contains(err.Error(), "primary email") {
|
||||
t.Fatalf("the lookup's reason was lost: %v", err)
|
||||
}
|
||||
if invites.calls != 0 {
|
||||
t.Fatal("a link was minted for an account that could not be found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResendWithNoMailConfiguredSaysSoRatherThanFailing(t *testing.T) {
|
||||
// Not an error: the tenant is fine and the server simply cannot send. The
|
||||
// controller turns this into a refusal for the operator, with the variable
|
||||
// named.
|
||||
service := NewTenantService(&inviteRepo{target: waiting()}, nil)
|
||||
|
||||
outcome, err := service.ResendInvite(1147)
|
||||
if err != nil {
|
||||
t.Fatalf("unconfigured mail reported as an error: %v", err)
|
||||
}
|
||||
if outcome.Sent || !strings.Contains(outcome.Reason, "not configured") {
|
||||
t.Fatalf("unhelpful outcome: %+v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResendReportsWhyTheMailWasRefused(t *testing.T) {
|
||||
invites := &countingInvites{sent: false, reason: "mailbox full"}
|
||||
service := NewTenantService(&inviteRepo{target: waiting()}, invites)
|
||||
|
||||
outcome, err := service.ResendInvite(1147)
|
||||
if err != nil {
|
||||
t.Fatalf("resend: %v", err)
|
||||
}
|
||||
if outcome.Sent || outcome.Reason != "mailbox full" {
|
||||
t.Fatalf("the provider's reason was lost: %+v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
// Onboarding and resend share the invite path, so a nil mailer must be safe on
|
||||
// both. This is the create side.
|
||||
func TestOnboardingWithNoMailStillCreatesTheTenant(t *testing.T) {
|
||||
_ = models.Tenants{}
|
||||
_ = config.MailConfig{}
|
||||
|
||||
service := NewTenantService(&inviteRepo{target: waiting()}, nil)
|
||||
outcome := service.(*tenantService).inviteFor(models.UserInfo{Userid: 904}, models.Tenants{})
|
||||
|
||||
if outcome.Sent {
|
||||
t.Fatal("reported as sent with no invite service")
|
||||
}
|
||||
if outcome.Reason == "" {
|
||||
t.Fatal("not sent, and no reason for the operator")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user