cast-ghl-plugin/internal/cast/client_test.go
Head of Product & Engineering 9995027093
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: per-location Cast API key and unified admin config API
Each GHL location can now have its own Cast API key and sender ID stored
in MongoDB. Falls back to global CAST_API_KEY / CAST_SENDER_ID env vars
when not set per-location.

Admin endpoints (all require Authorization: Bearer <INBOUND_API_KEY>):
  GET  /api/admin/locations                        — list all locations
  GET  /api/admin/locations/{locationId}/config    — get location config
  PUT  /api/admin/locations/{locationId}/config    — set sender_id + cast_api_key

Cast API key is masked in GET responses (first 12 chars + "...").
Replaces the /sender-id endpoint deployed in the previous commit.

Also adds FUTURE_DEV.md documenting the migration path to Infisical
for secret management, plus MongoDB security hardening checklist.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 14:08:29 +02:00

172 lines
5.4 KiB
Go

package cast
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
)
func TestSendSMS_Success(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/sms/send" {
t.Errorf("expected /api/sms/send, got %s", r.URL.Path)
}
if r.Header.Get("X-API-Key") != "cast_testkey" {
t.Errorf("expected X-API-Key cast_testkey, got %s", r.Header.Get("X-API-Key"))
}
var body SendRequest
_ = json.NewDecoder(r.Body).Decode(&body)
if body.To != "09171234567" {
t.Errorf("expected to=09171234567, got %s", body.To)
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(SendResponse{Success: true, MessageID: "abc123", Parts: 1})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_testkey", "")
resp, err := client.SendSMS(context.Background(), "09171234567", "test message", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !resp.Success || resp.MessageID != "abc123" {
t.Errorf("unexpected response: %+v", resp)
}
}
func TestSendSMS_APIError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusPaymentRequired)
_ = json.NewEncoder(w).Encode(SendResponse{Success: false, Error: "insufficient credits"})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_testkey", "")
_, err := client.SendSMS(context.Background(), "09171234567", "test", "", "")
if err == nil {
t.Fatal("expected error, got nil")
}
var castErr *CastAPIError
if !errors.As(err, &castErr) {
t.Fatalf("expected CastAPIError, got %T", err)
}
if castErr.StatusCode != http.StatusPaymentRequired {
t.Errorf("expected 402, got %d", castErr.StatusCode)
}
}
func TestSendSMS_SuccessFalseInBody(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(SendResponse{Success: false, Error: "invalid number"})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_testkey", "")
_, err := client.SendSMS(context.Background(), "09171234567", "test", "", "")
if err == nil {
t.Fatal("expected error, got nil")
}
var castErr *CastAPIError
if !errors.As(err, &castErr) {
t.Fatalf("expected CastAPIError, got %T", err)
}
if castErr.APIError != "invalid number" {
t.Errorf("expected 'invalid number', got %s", castErr.APIError)
}
}
func TestSendSMS_WithSenderID(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body SendRequest
_ = json.NewDecoder(r.Body).Decode(&body)
if body.SenderID != "CAST" {
t.Errorf("expected sender_id=CAST, got %q", body.SenderID)
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(SendResponse{Success: true, MessageID: "x1", Parts: 1})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_testkey", "CAST")
_, err := client.SendSMS(context.Background(), "09171234567", "test", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestSendSMS_WithoutSenderID(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var rawBody map[string]interface{}
_ = json.NewDecoder(r.Body).Decode(&rawBody)
if _, ok := rawBody["sender_id"]; ok {
t.Error("sender_id should be omitted when empty")
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(SendResponse{Success: true, MessageID: "x2", Parts: 1})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_testkey", "")
_, err := client.SendSMS(context.Background(), "09171234567", "test", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// TC5: Failed Cast API key — 401 response is returned as a CastAPIError.
func TestSendSMS_Unauthorized(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_ = json.NewEncoder(w).Encode(SendResponse{Success: false, Error: "invalid api key"})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_badkey", "")
_, err := client.SendSMS(context.Background(), "09171234567", "test", "", "")
if err == nil {
t.Fatal("expected error for 401, got nil")
}
var castErr *CastAPIError
if !errors.As(err, &castErr) {
t.Fatalf("expected CastAPIError, got %T: %v", err, err)
}
if castErr.StatusCode != http.StatusUnauthorized {
t.Errorf("expected status 401, got %d", castErr.StatusCode)
}
}
func TestSendSMS_RetryOn429(t *testing.T) {
var callCount atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n := callCount.Add(1)
if n <= 2 {
w.Header().Set("Retry-After", "0")
w.WriteHeader(http.StatusTooManyRequests)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(SendResponse{Success: true, MessageID: "retry-ok", Parts: 1})
}))
defer srv.Close()
client := NewClient(srv.URL, "cast_testkey", "")
resp, err := client.SendSMS(context.Background(), "09171234567", "test", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if resp.MessageID != "retry-ok" {
t.Errorf("expected retry-ok, got %s", resp.MessageID)
}
if callCount.Load() != 3 {
t.Errorf("expected 3 calls, got %d", callCount.Load())
}
}