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

211 lines
6.9 KiB
Go

package controllers
import (
"io"
"net/http/httptest"
"strings"
"testing"
"time"
"nearle/middleware"
"nearle/services"
"nearle/utils"
"github.com/gofiber/fiber/v2"
)
/*
Who may re-issue a first-password link, and for whom.
This endpoint mints a credential, so most of what matters is what it refuses.
The service layer refuses the business cases — an account that already has a
password, a tenant whose primary email matches no login — and those are covered
in `services/resendInvite_test.go`. This file is about the door: who gets
through it, and which account a request actually names.
*/
// resendService answers both resends and records which was called. Only the two
// methods under test are real; the rest of TenantService is embedded nil, which
// panics if anything else is reached — exactly the signal wanted.
type resendService struct {
services.TenantService
byTenant int
byUser int
outcome services.InviteOutcome
err error
}
func (s *resendService) ResendInvite(tenantID int) (services.InviteOutcome, error) {
s.byTenant = tenantID
return s.outcome, s.err
}
func (s *resendService) ResendInviteToUser(userID int) (services.InviteOutcome, error) {
s.byUser = userID
return s.outcome, s.err
}
func resendApp(t *testing.T, service *resendService) *fiber.App {
t.Helper()
t.Setenv("POS_TOKEN_SECRET", testSecret)
app := fiber.New()
// The real guard, mounted as routes.go mounts it: this endpoint sits behind
// the session, and the handler then requires a platform account on top.
app.Use("/live/api/v1/web", middleware.WebAuth(nil))
app.Post("/live/api/v1/web/tenants/resendinvite", NewTenantController(service).ResendInvite)
return app
}
// staffToken is a signed session for a Nearle staff account.
//
// `Superadmin` is the signal, and it is minted from `app_users.issuperadmin` —
// not from the tenant being zero and not from a role id. Both of those look
// equivalent and are not: `app_roles` calls roleid 1 "Super admin" and
// onboarding wrote 1 for every shop owner, and a zero tenant is what an
// unfilled column looks like. See `utils.WebClaims`.
func staffToken(t *testing.T) string {
t.Helper()
token, _, err := utils.MintWebToken(utils.WebClaims{
Userid: 12, Roleid: 1, Configid: 1, Superadmin: true,
}, time.Now())
if err != nil {
t.Fatalf("mint: %v", err)
}
return token
}
// merchantToken is a signed session for a shop's own admin.
func merchantToken(t *testing.T) string {
t.Helper()
token, _, err := utils.MintWebToken(utils.WebClaims{
Userid: 904, Tenantid: 1147, Roleid: 3, Configid: 1,
}, time.Now())
if err != nil {
t.Fatalf("mint: %v", err)
}
return token
}
func postAs(t *testing.T, app *fiber.App, token, body string) (int, string) {
t.Helper()
req := httptest.NewRequest("POST", "/live/api/v1/web/tenants/resendinvite",
strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("resendinvite: %v", err)
}
raw, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(raw)
}
func TestAMerchantCannotResendAnything(t *testing.T) {
// A merchant's session is pinned to their own tenant, so the worst they could
// do is re-invite themselves — and the service refuses that, because an
// account signing in to ask already has a password. Refusing here as well
// means the endpoint does not rely on two other checks to make the wrong case
// impossible.
service := &resendService{outcome: services.InviteOutcome{Sent: true}}
app := resendApp(t, service)
status, body := postAs(t, app, merchantToken(t), `{"tenantid":1147}`)
if status != 403 {
t.Fatalf("a merchant was let through: %d %s", status, body)
}
if service.byTenant != 0 || service.byUser != 0 {
t.Fatal("the service was reached by a caller who should have been refused")
}
}
func TestNearleStaffCanResendToATenantsOwner(t *testing.T) {
service := &resendService{outcome: services.InviteOutcome{Sent: true}}
app := resendApp(t, service)
status, body := postAs(t, app, staffToken(t), `{"tenantid":1147}`)
if status != 200 {
t.Fatalf("refused Nearle staff: %d %s", status, body)
}
if service.byTenant != 1147 {
t.Fatalf("resent for tenant %d, want 1147", service.byTenant)
}
}
func TestAUseridNamesOnePersonRatherThanTheOwner(t *testing.T) {
// The reason this parameter exists. Staff added after onboarding, and the
// login every branch spawns, are created with no password too — and a
// business has many of them, so "the tenant's invitation" cannot reach them.
service := &resendService{outcome: services.InviteOutcome{Sent: true}}
app := resendApp(t, service)
status, body := postAs(t, app, staffToken(t), `{"userid":7781}`)
if status != 200 {
t.Fatalf("refused: %d %s", status, body)
}
if service.byUser != 7781 {
t.Fatalf("resent for user %d, want 7781", service.byUser)
}
if service.byTenant != 0 {
t.Fatal("emailed the owner when a person was named")
}
}
func TestAUseridWinsOverATenantid(t *testing.T) {
// A caller that sent a person's id meant that person. Falling back to the
// owner would be the wrong mailbox with nothing on the response to say so.
service := &resendService{outcome: services.InviteOutcome{Sent: true}}
app := resendApp(t, service)
if status, body := postAs(t, app, staffToken(t), `{"tenantid":1147,"userid":7781}`); status != 200 {
t.Fatalf("refused: %d %s", status, body)
}
if service.byUser != 7781 || service.byTenant != 0 {
t.Fatalf("resolved to the wrong account: user=%d tenant=%d", service.byUser, service.byTenant)
}
}
func TestAnEmptyBodyIsRefusedRatherThanSentToTenantZero(t *testing.T) {
// `{}` parses cleanly into two zeroes. Without this check it would reach the
// service as tenant 0 and come back "tenant 0 has no account matching its
// primary email address", which describes nothing the caller did.
service := &resendService{outcome: services.InviteOutcome{Sent: true}}
app := resendApp(t, service)
status, body := postAs(t, app, staffToken(t), `{}`)
if status != 400 {
t.Fatalf("an empty request was accepted: %d %s", status, body)
}
if service.byTenant != 0 || service.byUser != 0 {
t.Fatal("the service was called with nothing to act on")
}
if !strings.Contains(body, "tenantid") || !strings.Contains(body, "userid") {
t.Errorf("the refusal does not say what to send: %s", body)
}
}
func TestMailThatDidNotLeaveIsReportedAsAFailure(t *testing.T) {
// The operator pressed a button expecting an email to go. "Success" with no
// mail sent is the one answer they cannot act on.
service := &resendService{outcome: services.InviteOutcome{
Sent: false, Reason: "MAIL_HOST is not set",
}}
app := resendApp(t, service)
status, body := postAs(t, app, staffToken(t), `{"tenantid":1147}`)
if status != 409 {
t.Fatalf("an unsent invitation was reported as sent: %d %s", status, body)
}
if !strings.Contains(body, "MAIL_HOST") {
t.Errorf("the reason was lost: %s", body)
}
}