Files
doormile_backend/internal/sms/sms_test.go

141 lines
4.1 KiB
Go

package sms
import (
"os"
"strings"
"testing"
)
// StagingCode is a permanent skeleton key for every account on the platform if
// it ever reaches production. The guard against that is the only thing standing
// between a convenience for QA and a total auth bypass, so it is tested rather
// than trusted.
func setEnv(t *testing.T, key, value string) {
t.Helper()
previous, had := os.LookupEnv(key)
if value == "" {
_ = os.Unsetenv(key)
} else {
_ = os.Setenv(key, value)
}
t.Cleanup(func() {
if had {
_ = os.Setenv(key, previous)
} else {
_ = os.Unsetenv(key)
}
})
}
// A fixed OTP is refused in production however the environment is spelt.
func TestStagingCodeIsRefusedInProduction(t *testing.T) {
for _, env := range []string{"production", "PRODUCTION", "Production", " production "} {
setEnv(t, "CX_STAGING_OTP", "1234")
setEnv(t, "ENV", env)
if got := StagingCode(); got != "" {
t.Errorf("ENV=%q returned the fixed code %q — that is a skeleton key "+
"for every account on the platform", env, got)
}
}
}
// And is available everywhere else, which is what unblocks automated sign-in.
func TestStagingCodeIsAvailableOutsideProduction(t *testing.T) {
for _, env := range []string{"development", "staging", ""} {
setEnv(t, "CX_STAGING_OTP", "1234")
setEnv(t, "ENV", env)
if got := StagingCode(); got != "1234" {
t.Errorf("ENV=%q returned %q, want the configured staging code", env, got)
}
}
}
// Unset means unset — no accidental default.
func TestStagingCodeIsEmptyWhenNotConfigured(t *testing.T) {
setEnv(t, "ENV", "development")
setEnv(t, "CX_STAGING_OTP", "")
if got := StagingCode(); got != "" {
t.Errorf("StagingCode() = %q with nothing configured, want empty", got)
}
}
// Until a gateway is registered, Configured() must report false. Shipping while
// this quietly said true would mean nobody noticed OTP codes were only reaching
// the application log.
func TestTransportReportsThatNoGatewayIsWired(t *testing.T) {
if Configured() {
t.Error("Configured() = true with no gateway registered — " +
"the log sink must never claim to be a real transport")
}
if Transport() != "log" {
t.Errorf("Transport() = %q, want \"log\"", Transport())
}
}
// Registering a gateway flips both, and Register(nil) is ignored rather than
// silently disabling delivery.
func TestRegisterInstallsAGatewayAndIgnoresNil(t *testing.T) {
original := active
t.Cleanup(func() { active = original })
Register(nil)
if Transport() != "log" {
t.Errorf("Register(nil) changed the transport to %q", Transport())
}
fake := &recordingSender{}
Register(fake)
if !Configured() || Transport() != "test" {
t.Fatalf("after Register: configured=%v transport=%q", Configured(), Transport())
}
if err := SendOTP("+919876543210", "4821"); err != nil {
t.Fatalf("SendOTP: %v", err)
}
if fake.phone != "+919876543210" {
t.Errorf("phone = %q, want the number passed in", fake.phone)
}
if !strings.Contains(fake.message, "4821") {
t.Errorf("message %q does not carry the code", fake.message)
}
if !strings.Contains(fake.message, "Do not share") {
t.Errorf("message %q is missing the do-not-share warning", fake.message)
}
}
// An empty number is refused rather than handed to a gateway that will bill for
// it and fail.
func TestSendOTPRefusesAnEmptyNumber(t *testing.T) {
if err := SendOTP(" ", "4821"); err == nil {
t.Error("SendOTP accepted an empty phone number")
}
}
// The log sink masks the number. An OTP in a log file is already bad enough
// without the number it belongs to sitting beside it.
func TestMaskPhoneHidesTheSubscriberDigits(t *testing.T) {
got := maskPhone("+919876543210")
if strings.Contains(got, "9876543") {
t.Errorf("maskPhone = %q, still exposes the subscriber digits", got)
}
if !strings.HasSuffix(got, "10") {
t.Errorf("maskPhone = %q, should keep the last two digits so a support "+
"call can be matched", got)
}
}
type recordingSender struct {
phone string
message string
}
func (r *recordingSender) Name() string { return "test" }
func (r *recordingSender) Send(phone, message string) error {
r.phone, r.message = phone, message
return nil
}