package ghl import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "net/http" "net/http/httptest" "strings" "testing" castclient "git.sds.dev/CAST/cast-ghl-plugin/internal/cast" "git.sds.dev/CAST/cast-ghl-plugin/internal/store" ) func generateTestKeyPair(t *testing.T) (*ecdsa.PrivateKey, string) { t.Helper() privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("failed to generate key: %v", err) } pubBytes, err := x509.MarshalPKIXPublicKey(&privKey.PublicKey) if err != nil { t.Fatalf("failed to marshal public key: %v", err) } pemBlock := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes}) return privKey, string(pemBlock) } func signPayload(t *testing.T, privKey *ecdsa.PrivateKey, body []byte) string { t.Helper() hash := sha256.Sum256(body) sig, err := ecdsa.SignASN1(rand.Reader, privKey, hash[:]) if err != nil { t.Fatalf("failed to sign: %v", err) } return base64.StdEncoding.EncodeToString(sig) } func newTestHandler(t *testing.T, pubPEM string) *WebhookHandler { t.Helper() ms := &inMemStore{} oauth := NewOAuthHandler("c", "s", "http://x", "p", ms) handler, err := NewWebhookHandler(pubPEM, castclient.NewClient("http://localhost:1", "k", ""), NewAPIClient(), oauth, ms) if err != nil { t.Fatalf("failed to create handler: %v", err) } return handler } func TestWebhook_ValidSignature_SMS(t *testing.T) { privKey, pubPEM := generateTestKeyPair(t) handler := newTestHandler(t, pubPEM) body := `{"contactId":"c1","locationId":"loc1","messageId":"msg1","type":"SMS","phone":"+639171234567","message":"hello","attachments":[],"userId":"u1"}` sig := signPayload(t, privKey, []byte(body)) req := httptest.NewRequest(http.MethodPost, "/api/ghl/v1/webhook/messages", strings.NewReader(body)) req.Header.Set("x-wh-signature", sig) rr := httptest.NewRecorder() handler.HandleWebhook(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } } func TestWebhook_InvalidSignature(t *testing.T) { _, pubPEM := generateTestKeyPair(t) handler := newTestHandler(t, pubPEM) body := `{"type":"SMS","phone":"+639171234567","message":"test"}` req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) req.Header.Set("x-wh-signature", "aW52YWxpZA==") rr := httptest.NewRecorder() handler.HandleWebhook(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rr.Code) } } func TestWebhook_MissingSignature(t *testing.T) { _, pubPEM := generateTestKeyPair(t) handler := newTestHandler(t, pubPEM) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"type":"SMS"}`)) rr := httptest.NewRecorder() handler.HandleWebhook(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rr.Code) } } func TestWebhook_NonSMSType(t *testing.T) { privKey, pubPEM := generateTestKeyPair(t) handler := newTestHandler(t, pubPEM) body := `{"type":"Email","phone":"+639171234567","message":"test"}` sig := signPayload(t, privKey, []byte(body)) req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) req.Header.Set("x-wh-signature", sig) rr := httptest.NewRecorder() handler.HandleWebhook(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d", rr.Code) } } // TC9: Duplicate webhook delivery — second delivery of same messageId is silently accepted (200). func TestWebhook_DuplicateMessageID(t *testing.T) { privKey, pubPEM := generateTestKeyPair(t) handler := newTestHandler(t, pubPEM) body := `{"contactId":"c1","locationId":"loc1","messageId":"dup-msg-1","type":"SMS","phone":"+639171234567","message":"hello","attachments":[],"userId":"u1"}` sig := signPayload(t, privKey, []byte(body)) sendRequest := func() *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/api/ghl/v1/webhook/messages", strings.NewReader(body)) req.Header.Set("x-wh-signature", sig) rr := httptest.NewRecorder() handler.HandleWebhook(rr, req) return rr } rr1 := sendRequest() if rr1.Code != http.StatusOK { t.Errorf("first delivery: expected 200, got %d", rr1.Code) } rr2 := sendRequest() if rr2.Code != http.StatusOK { t.Errorf("duplicate delivery: expected 200 (idempotent), got %d", rr2.Code) } } // TC11: 450-char boundary message — webhook accepts and returns 200. func TestWebhook_450CharMessage(t *testing.T) { privKey, pubPEM := generateTestKeyPair(t) handler := newTestHandler(t, pubPEM) msg450 := strings.Repeat("x", 450) payload := `{"contactId":"c1","locationId":"loc1","messageId":"msg-450","type":"SMS","phone":"+639171234567","message":"` + msg450 + `","attachments":[],"userId":"u1"}` sig := signPayload(t, privKey, []byte(payload)) req := httptest.NewRequest(http.MethodPost, "/api/ghl/v1/webhook/messages", strings.NewReader(payload)) req.Header.Set("x-wh-signature", sig) rr := httptest.NewRecorder() handler.HandleWebhook(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200 for 450-char message, got %d", rr.Code) } } // TC12: Uninstall — valid signature deletes token. func TestHandleUninstall_ValidSignature(t *testing.T) { privKey, pubPEM := generateTestKeyPair(t) ms := &inMemStore{ token: &store.TokenRecord{LocationID: "loc-uninstall"}, } oauth := NewOAuthHandler("c", "s", "http://x", "p", ms) handler, err := NewWebhookHandler(pubPEM, castclient.NewClient("http://localhost:1", "k", ""), NewAPIClient(), oauth, ms) if err != nil { t.Fatalf("failed to create handler: %v", err) } body := `{"locationId":"loc-uninstall","companyId":"comp1"}` sig := signPayload(t, privKey, []byte(body)) req := httptest.NewRequest(http.MethodPost, "/api/ghl/v1/uninstall", strings.NewReader(body)) req.Header.Set("x-wh-signature", sig) rr := httptest.NewRecorder() handler.HandleUninstall(rr, req) if rr.Code != http.StatusOK { t.Errorf("expected 200, got %d (body: %s)", rr.Code, rr.Body.String()) } // Verify token was deleted if ms.token != nil { t.Errorf("expected token to be deleted after uninstall, still present") } } // TC12: Uninstall — invalid signature returns 401. func TestHandleUninstall_InvalidSignature(t *testing.T) { _, pubPEM := generateTestKeyPair(t) ms := &inMemStore{} oauth := NewOAuthHandler("c", "s", "http://x", "p", ms) handler, err := NewWebhookHandler(pubPEM, castclient.NewClient("http://localhost:1", "k", ""), NewAPIClient(), oauth, ms) if err != nil { t.Fatalf("failed to create handler: %v", err) } body := `{"locationId":"loc1","companyId":"comp1"}` req := httptest.NewRequest(http.MethodPost, "/api/ghl/v1/uninstall", strings.NewReader(body)) req.Header.Set("x-wh-signature", "aW52YWxpZA==") rr := httptest.NewRecorder() handler.HandleUninstall(rr, req) if rr.Code != http.StatusUnauthorized { t.Errorf("expected 401, got %d", rr.Code) } }