Files
backend_fiesta/services/resendInvite_test.go
2026-09-29 16:53:21 +05:30

169 lines
5.3 KiB
Go

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")
}
}