187 lines
5.5 KiB
Go
187 lines
5.5 KiB
Go
package sms
|
|
|
|
import (
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The gateway that closes the sign-in blocker.
|
|
//
|
|
// sms.Register had no callers anywhere in the tree, so logSender was never
|
|
// replaced and every customer verification code went to the application log
|
|
// instead of to a phone. That is why nobody could sign in, why the app's
|
|
// offline dev mode became the only practical way in, and why bookings "made"
|
|
// in that mode never reached the admin console.
|
|
|
|
func restoreSender(t *testing.T) {
|
|
t.Helper()
|
|
previous := active
|
|
t.Cleanup(func() { active = previous })
|
|
}
|
|
|
|
func testSender(url, bodyTmpl string) httpSender {
|
|
if bodyTmpl == "" {
|
|
bodyTmpl = `{"to":"{{phone}}","message":"{{message}}","sender":"{{sender}}"}`
|
|
}
|
|
return httpSender{
|
|
url: url,
|
|
method: http.MethodPost,
|
|
bodyTmpl: bodyTmpl,
|
|
contentType: "application/json",
|
|
senderID: "DRMILE",
|
|
client: &http.Client{Timeout: 5 * time.Second},
|
|
}
|
|
}
|
|
|
|
// The destination and the code have to actually reach the gateway.
|
|
func TestGatewaySendsPhoneAndCode(t *testing.T) {
|
|
var got string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
got = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
restoreSender(t)
|
|
Register(testSender(srv.URL, ""))
|
|
|
|
if err := SendOTP("+919876543210", "4821"); err != nil {
|
|
t.Fatalf("SendOTP: %v", err)
|
|
}
|
|
for _, want := range []string{"+919876543210", "4821", "DRMILE"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("gateway body %q is missing %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A refused send — no balance, unapproved DLT template — must surface as an
|
|
// error. Swallowing it tells the customer a code is on its way when it is not,
|
|
// which is precisely the failure logSender has been producing all along.
|
|
func TestGatewayRefusalIsReported(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusPaymentRequired)
|
|
_, _ = w.Write([]byte(`{"error":"insufficient balance"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
restoreSender(t)
|
|
Register(testSender(srv.URL, ""))
|
|
|
|
if err := SendOTP("+919876543210", "4821"); err == nil {
|
|
t.Error("a rejected send reported success — the customer would wait for a " +
|
|
"text that is never coming")
|
|
}
|
|
}
|
|
|
|
// An unreachable gateway is an error, not a silent no-op.
|
|
func TestUnreachableGatewayIsReported(t *testing.T) {
|
|
restoreSender(t)
|
|
Register(testSender("http://127.0.0.1:1/unreachable", ""))
|
|
|
|
if err := SendOTP("+919876543210", "4821"); err == nil {
|
|
t.Error("an unreachable gateway reported success")
|
|
}
|
|
}
|
|
|
|
// A quote in the message must not break a JSON body template.
|
|
func TestMessageIsEscapedForJSON(t *testing.T) {
|
|
var got string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
got = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
restoreSender(t)
|
|
Register(testSender(srv.URL, ""))
|
|
|
|
if err := active.Send("+919876543210", `say "hello"`); err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if strings.Contains(got, `say "hello"`) {
|
|
t.Errorf("an unescaped quote reached the JSON body: %q", got)
|
|
}
|
|
if !strings.Contains(got, `say \"hello\"`) {
|
|
t.Errorf("the message was not escaped as expected: %q", got)
|
|
}
|
|
}
|
|
|
|
// Vendors differ; the template is what makes one sender cover all of them.
|
|
func TestBodyTemplateIsVendorAgnostic(t *testing.T) {
|
|
var got string
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
b, _ := io.ReadAll(r.Body)
|
|
got = string(b)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
restoreSender(t)
|
|
Register(testSender(srv.URL, "mobiles={{phone}}&message={{message}}&sender={{sender}}"))
|
|
|
|
if err := SendOTP("+919876543210", "4821"); err != nil {
|
|
t.Fatalf("send: %v", err)
|
|
}
|
|
if !strings.HasPrefix(got, "mobiles=+919876543210&message=") {
|
|
t.Errorf("form-encoded template not honoured: %q", got)
|
|
}
|
|
}
|
|
|
|
// Configure is what gives sms.Register its first caller. With no URL it must
|
|
// leave the log sink exactly where it is, so this change cannot break a
|
|
// deployment that has not been configured yet.
|
|
func TestConfigureLeavesTheLogSinkWhenUnset(t *testing.T) {
|
|
restoreSender(t)
|
|
active = logSender{}
|
|
setEnv(t, "SMS_GATEWAY_URL", "")
|
|
|
|
Configure()
|
|
|
|
if Configured() {
|
|
t.Error("Configure installed a gateway with no SMS_GATEWAY_URL set")
|
|
}
|
|
if Transport() != "log" {
|
|
t.Errorf("Transport() = %q, want \"log\"", Transport())
|
|
}
|
|
}
|
|
|
|
func TestConfigureInstallsTheGatewayWhenSet(t *testing.T) {
|
|
restoreSender(t)
|
|
active = logSender{}
|
|
setEnv(t, "SMS_GATEWAY_URL", "https://sms.example.invalid/send")
|
|
|
|
Configure()
|
|
|
|
if !Configured() {
|
|
t.Fatal("Configure did not install a gateway despite SMS_GATEWAY_URL being set")
|
|
}
|
|
if Transport() != "http-gateway" {
|
|
t.Errorf("Transport() = %q, want \"http-gateway\"", Transport())
|
|
}
|
|
}
|
|
|
|
// In production the log sink must refuse rather than write a live credential
|
|
// to the log and report success.
|
|
func TestLogSenderRefusesInProduction(t *testing.T) {
|
|
restoreSender(t)
|
|
active = logSender{}
|
|
|
|
setEnv(t, "ENV", "production")
|
|
if err := SendOTP("+919876543210", "4821"); err == nil {
|
|
t.Error("with no gateway in production, SendOTP reported success — the code " +
|
|
"went to the log and the customer was told it was sent")
|
|
}
|
|
|
|
setEnv(t, "ENV", "development")
|
|
if err := SendOTP("+919876543210", "4821"); err != nil {
|
|
t.Errorf("outside production the log sink must still work for QA: %v", err)
|
|
}
|
|
}
|