Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/security"
|
||||
)
|
||||
|
||||
// --- Registry Tests ---
|
||||
|
||||
func TestWebhookProcessorRegistry_RegisterAndGet(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
reg := NewWebhookProcessorRegistry(signer, nil, nil)
|
||||
|
||||
slackProc, ok := reg.Get(model.HookTypeSlack)
|
||||
assert.True(t, ok, "Slack processor should be registered")
|
||||
assert.Equal(t, model.HookTypeSlack, slackProc.HookType())
|
||||
|
||||
shopifyProc, ok := reg.Get(model.HookTypeShopify)
|
||||
assert.True(t, ok, "Shopify processor should be registered")
|
||||
assert.Equal(t, model.HookTypeShopify, shopifyProc.HookType())
|
||||
|
||||
linearProc, ok := reg.Get(model.HookTypeLinear)
|
||||
assert.True(t, ok, "Linear processor should be registered")
|
||||
assert.Equal(t, model.HookTypeLinear, linearProc.HookType())
|
||||
|
||||
notionProc, ok := reg.Get(model.HookTypeNotion)
|
||||
assert.True(t, ok, "Notion processor should be registered")
|
||||
assert.Equal(t, model.HookTypeNotion, notionProc.HookType())
|
||||
|
||||
webhookProc, ok := reg.Get(model.HookTypeWebhook)
|
||||
assert.True(t, ok, "Generic webhook processor should be registered")
|
||||
assert.Equal(t, model.HookTypeWebhook, webhookProc.HookType())
|
||||
|
||||
_, ok = reg.Get(model.HookType("unknown"))
|
||||
assert.False(t, ok, "Unknown hook type should not be registered")
|
||||
}
|
||||
|
||||
func TestWebhookProcessorRegistry_ListTypes(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
reg := NewWebhookProcessorRegistry(signer, nil, nil)
|
||||
types := reg.ListTypes()
|
||||
assert.Len(t, types, 5)
|
||||
}
|
||||
|
||||
func TestWebhookProcessorRegistry_CustomRegistration(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
reg := &WebhookProcessorRegistry{processors: make(map[model.HookType]WebhookEventProcessor)}
|
||||
reg.Register(NewSlackEventProcessor(nil, nil, signer))
|
||||
proc, ok := reg.Get(model.HookTypeSlack)
|
||||
assert.True(t, ok)
|
||||
assert.NotNil(t, proc)
|
||||
}
|
||||
|
||||
// --- Slack Processor Tests ---
|
||||
|
||||
func TestSlackEventProcessor_URLVerification(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 1, HookType: model.HookTypeSlack, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": "url_verification",
|
||||
"challenge": "test_challenge_token_123",
|
||||
"token": "verification_token",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.Error(t, err, "URL verification should return special error")
|
||||
assert.Contains(t, err.Error(), "slack_url_verification")
|
||||
assert.Contains(t, err.Error(), "test_challenge_token_123")
|
||||
}
|
||||
|
||||
func TestSlackEventProcessor_MessageEvent(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 2, HookType: model.HookTypeSlack, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"event": map[string]interface{}{
|
||||
"type": "message",
|
||||
"user": "U12345",
|
||||
"channel": "C67890",
|
||||
"text": "Hello from Slack!",
|
||||
"ts": "1234567890.123456",
|
||||
},
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err, "Valid Slack message event should process successfully")
|
||||
}
|
||||
|
||||
func TestSlackEventProcessor_EmptyMessage(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 3, HookType: model.HookTypeSlack, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"event": map[string]interface{}{
|
||||
"type": "message",
|
||||
"user": "U12345",
|
||||
"channel": "C67890",
|
||||
"text": "",
|
||||
},
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.Error(t, err, "Empty Slack message should be rejected")
|
||||
assert.Contains(t, err.Error(), "empty")
|
||||
}
|
||||
|
||||
func TestSlackEventProcessor_MissingEventField(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 4, HookType: model.HookTypeSlack, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": "event_callback",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.Error(t, err, "Payload without 'event' field should be rejected")
|
||||
assert.Contains(t, err.Error(), "missing 'event' field")
|
||||
}
|
||||
|
||||
func TestSlackEventProcessor_ChannelJoinLeave(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 5, HookType: model.HookTypeSlack, Status: model.HookStatusActive}
|
||||
|
||||
for _, eventType := range []string{"channel_join", "channel_leave"} {
|
||||
payload := map[string]interface{}{
|
||||
"event": map[string]interface{}{
|
||||
"type": eventType,
|
||||
},
|
||||
}
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err, "Channel join/leave events should be accepted (no action needed)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlackEventProcessor_UnsupportedEventType(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 6, HookType: model.HookTypeSlack, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"event": map[string]interface{}{
|
||||
"type": "reaction_added",
|
||||
},
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err, "Unsupported Slack events should be silently accepted")
|
||||
}
|
||||
|
||||
// --- Shopify Processor Tests ---
|
||||
|
||||
func TestShopifyEventProcessor_OrderCreate(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewShopifyEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 10, HookType: model.HookTypeShopify, Status: model.HookStatusActive}
|
||||
|
||||
for _, topic := range []string{"orders/create", "orders/updated", "orders/paid"} {
|
||||
payload := map[string]interface{}{
|
||||
"_shopify_topic": topic,
|
||||
"id": float64(12345),
|
||||
"email": "customer@example.com",
|
||||
}
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err, "Shopify %s should process successfully", topic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShopifyEventProcessor_CustomerCreate(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewShopifyEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 11, HookType: model.HookTypeShopify, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"_shopify_topic": "customers/create",
|
||||
"id": float64(67890),
|
||||
"email": "newcustomer@example.com",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestShopifyEventProcessor_UnsupportedTopic(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewShopifyEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 12, HookType: model.HookTypeShopify, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"_shopify_topic": "products/create",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err, "Unsupported Shopify topics should be silently accepted")
|
||||
}
|
||||
|
||||
// --- Linear Processor Tests ---
|
||||
|
||||
func TestLinearEventProcessor_IssueEvent(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewLinearEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 20, HookType: model.HookTypeLinear, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"action": "create",
|
||||
"data": map[string]interface{}{
|
||||
"entityType": "Issue",
|
||||
"id": "abc-123",
|
||||
"title": "Bug report: login fails",
|
||||
},
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLinearEventProcessor_CommentEvent(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewLinearEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 21, HookType: model.HookTypeLinear, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"action": "create",
|
||||
"data": map[string]interface{}{
|
||||
"entityType": "Comment",
|
||||
"id": "comment-456",
|
||||
"body": "This is a comment",
|
||||
},
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLinearEventProcessor_MissingDataField(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewLinearEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 22, HookType: model.HookTypeLinear, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"action": "create",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.Error(t, err, "Linear payload without 'data' should be rejected")
|
||||
assert.Contains(t, err.Error(), "missing 'data' field")
|
||||
}
|
||||
|
||||
// --- Notion Processor Tests ---
|
||||
|
||||
func TestNotionEventProcessor_BasicEvent(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewNotionEventProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 30, HookType: model.HookTypeNotion, Status: model.HookStatusActive}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"type": "page.created",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// --- Generic Webhook Processor Tests ---
|
||||
|
||||
func TestGenericWebhookProcessor_BasicEvent(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewGenericWebhookProcessor(nil, signer)
|
||||
hook := &model.IntegrationHook{ID: 40, HookType: model.HookTypeWebhook, Status: model.HookStatusActive, AccessToken: "test_secret_unique_webhook_001"}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"event": "custom_event",
|
||||
"data": "some data",
|
||||
}
|
||||
|
||||
err := proc.ProcessEvent(context.Background(), hook, payload)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// --- ParseWebhookPayload Tests ---
|
||||
|
||||
func TestParseWebhookPayload_ValidJSON(t *testing.T) {
|
||||
body := []byte(`{"type":"message","text":"hello","user":"U123"}`)
|
||||
payload, err := ParseWebhookPayload(body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "message", payload["type"])
|
||||
assert.Equal(t, "hello", payload["text"])
|
||||
}
|
||||
|
||||
func TestParseWebhookPayload_InvalidJSON(t *testing.T) {
|
||||
body := []byte(`{invalid json}`)
|
||||
payload, err := ParseWebhookPayload(body)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, payload)
|
||||
}
|
||||
|
||||
func TestParseWebhookPayload_EmptyBody(t *testing.T) {
|
||||
body := []byte(``)
|
||||
payload, err := ParseWebhookPayload(body)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, payload)
|
||||
}
|
||||
|
||||
// --- Signature Verification Integration Tests ---
|
||||
// These tests verify that the VerifySignature method correctly integrates
|
||||
// with the WebhookSignatureService for each channel type.
|
||||
|
||||
func TestSlackEventProcessor_VerifySignature_InvalidSignature(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewSlackEventProcessor(nil, nil, signer)
|
||||
|
||||
hook := &model.IntegrationHook{ID: 1, AccessToken: "slack_secret_token_123_unique"}
|
||||
body := []byte(`{"type":"event_callback","event":{"type":"message"}}`)
|
||||
|
||||
r := httptest.NewRequest("POST", "/webhooks/slack", bytes.NewReader(body))
|
||||
r.Header.Set("X-Slack-Signature", "v0=invalid_signature_hash")
|
||||
r.Header.Set("X-Slack-Request-Timestamp", "1234567890")
|
||||
|
||||
err := proc.VerifySignature(r, body, hook)
|
||||
assert.Error(t, err, "Invalid Slack signature should be rejected")
|
||||
}
|
||||
|
||||
func TestShopifyEventProcessor_VerifySignature_InvalidSignature(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewShopifyEventProcessor(nil, signer)
|
||||
|
||||
hook := &model.IntegrationHook{ID: 2, AccessToken: "shopify_shared_secret_unique_456"}
|
||||
body := []byte(`{"id":12345,"email":"test@example.com"}`)
|
||||
|
||||
r := httptest.NewRequest("POST", "/webhooks/shopify", bytes.NewReader(body))
|
||||
r.Header.Set("X-Shopify-Hmac-Sha256", "invalid_base64_signature")
|
||||
|
||||
err := proc.VerifySignature(r, body, hook)
|
||||
assert.Error(t, err, "Invalid Shopify signature should be rejected")
|
||||
}
|
||||
|
||||
func TestLinearEventProcessor_VerifySignature_InvalidSignature(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewLinearEventProcessor(nil, signer)
|
||||
|
||||
hook := &model.IntegrationHook{ID: 3, AccessToken: "linear_webhook_secret_unique_789"}
|
||||
body := []byte(`{"action":"create","data":{"entityType":"Issue"}}`)
|
||||
|
||||
r := httptest.NewRequest("POST", "/webhooks/linear", bytes.NewReader(body))
|
||||
r.Header.Set("X-Linear-Signature", "invalid_hex_signature")
|
||||
|
||||
err := proc.VerifySignature(r, body, hook)
|
||||
assert.Error(t, err, "Invalid Linear signature should be rejected")
|
||||
}
|
||||
|
||||
func TestGenericWebhookProcessor_VerifySignature_InvalidSignature(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewGenericWebhookProcessor(nil, signer)
|
||||
|
||||
hook := &model.IntegrationHook{ID: 4, AccessToken: "generic_webhook_secret_unique_012"}
|
||||
body := []byte(`{"event":"custom_event"}`)
|
||||
|
||||
r := httptest.NewRequest("POST", "/webhooks/generic", bytes.NewReader(body))
|
||||
r.Header.Set("X-Webhook-Hmac-Signature", "invalid_hmac_hex")
|
||||
|
||||
err := proc.VerifySignature(r, body, hook)
|
||||
assert.Error(t, err, "Invalid generic webhook signature should be rejected")
|
||||
}
|
||||
|
||||
func TestGenericWebhookProcessor_VerifySignature_EmptySecret(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewGenericWebhookProcessor(nil, signer)
|
||||
|
||||
hook := &model.IntegrationHook{ID: 5, AccessToken: ""}
|
||||
body := []byte(`{"event":"test"}`)
|
||||
|
||||
r := httptest.NewRequest("POST", "/webhooks/generic", bytes.NewReader(body))
|
||||
r.Header.Set("X-Webhook-Hmac-Signature", "some_signature")
|
||||
|
||||
err := proc.VerifySignature(r, body, hook)
|
||||
assert.Error(t, err, "Empty secret should be rejected")
|
||||
}
|
||||
|
||||
func TestGenericWebhookProcessor_VerifySignature_EmptyPayload(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
proc := NewGenericWebhookProcessor(nil, signer)
|
||||
|
||||
hook := &model.IntegrationHook{ID: 6, AccessToken: "secret_for_empty_payload_test"}
|
||||
body := []byte{}
|
||||
|
||||
r := httptest.NewRequest("POST", "/webhooks/generic", bytes.NewReader(body))
|
||||
r.Header.Set("X-Webhook-Hmac-Signature", "some_signature")
|
||||
|
||||
err := proc.VerifySignature(r, body, hook)
|
||||
assert.Error(t, err, "Empty payload should be rejected")
|
||||
}
|
||||
|
||||
// --- ProcessWebhookEvent Integration Tests (using IntegrationHookService) ---
|
||||
// These tests verify the full flow: hook lookup → status check → signature verify → event dispatch
|
||||
|
||||
func TestProcessWebhookEvent_InactiveHook(t *testing.T) {
|
||||
// Inactive hooks should be rejected before signature verification
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
hookRepo := createTestHookRepo(t)
|
||||
appRepo := createTestAppRepo(t)
|
||||
reg := NewWebhookProcessorRegistry(signer, hookRepo, nil)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, reg)
|
||||
|
||||
ctx := context.Background()
|
||||
hook := &model.IntegrationHook{
|
||||
AccountID: 1,
|
||||
HookType: model.HookTypeSlack,
|
||||
Status: model.HookStatusInactive,
|
||||
AccessToken: "slack_inactive_hook_secret_unique_001",
|
||||
}
|
||||
err := hookRepo.Create(ctx, hook)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := []byte(`{"type":"url_verification","challenge":"test"}`)
|
||||
r := httptest.NewRequest("POST", "/webhooks/slack", bytes.NewReader(body))
|
||||
payload := map[string]interface{}{"type": "url_verification", "challenge": "test"}
|
||||
|
||||
err = svc.ProcessWebhookEvent(ctx, hook.ID, payload, r, body)
|
||||
assert.Error(t, err, "Inactive hook should be rejected")
|
||||
assert.Contains(t, err.Error(), "inactive")
|
||||
}
|
||||
|
||||
func TestProcessWebhookEvent_UnknownHookType(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
hookRepo := createTestHookRepo(t)
|
||||
appRepo := createTestAppRepo(t)
|
||||
reg := NewWebhookProcessorRegistry(signer, hookRepo, nil)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, reg)
|
||||
|
||||
ctx := context.Background()
|
||||
hook := &model.IntegrationHook{
|
||||
AccountID: 1,
|
||||
HookType: model.HookType("unknown_type"),
|
||||
Status: model.HookStatusActive,
|
||||
AccessToken: "unknown_hook_secret_unique_002",
|
||||
}
|
||||
err := hookRepo.Create(ctx, hook)
|
||||
require.NoError(t, err)
|
||||
|
||||
body := []byte(`{"event":"test"}`)
|
||||
r := httptest.NewRequest("POST", "/webhooks/unknown", bytes.NewReader(body))
|
||||
payload := map[string]interface{}{"event": "test"}
|
||||
|
||||
err = svc.ProcessWebhookEvent(ctx, hook.ID, payload, r, body)
|
||||
assert.Error(t, err, "Unknown hook type should be rejected")
|
||||
assert.Contains(t, err.Error(), "no processor")
|
||||
}
|
||||
|
||||
func TestProcessWebhookEvent_HookNotFound(t *testing.T) {
|
||||
signer := security.NewWebhookSignatureService(security.DefaultWebhookSignatureConfig())
|
||||
hookRepo := createTestHookRepo(t)
|
||||
appRepo := createTestAppRepo(t)
|
||||
reg := NewWebhookProcessorRegistry(signer, hookRepo, nil)
|
||||
svc := NewIntegrationHookService(hookRepo, appRepo, reg)
|
||||
|
||||
ctx := context.Background()
|
||||
body := []byte(`{"event":"test"}`)
|
||||
r := httptest.NewRequest("POST", "/webhooks/999999", bytes.NewReader(body))
|
||||
payload := map[string]interface{}{"event": "test"}
|
||||
|
||||
err := svc.ProcessWebhookEvent(ctx, 999999, payload, r, body)
|
||||
assert.Error(t, err, "Non-existent hook ID should be rejected")
|
||||
assert.Contains(t, err.Error(), "not found")
|
||||
}
|
||||
|
||||
// --- Utility function tests ---
|
||||
|
||||
func TestPayloadKeys(t *testing.T) {
|
||||
m := map[string]interface{}{
|
||||
"type": "message",
|
||||
"text": "hello",
|
||||
"user": "U123",
|
||||
}
|
||||
keys := payloadKeys(m)
|
||||
assert.Len(t, keys, 3)
|
||||
assert.Contains(t, keys, "type")
|
||||
assert.Contains(t, keys, "text")
|
||||
assert.Contains(t, keys, "user")
|
||||
}
|
||||
|
||||
func TestPayloadKeys_Empty(t *testing.T) {
|
||||
keys := payloadKeys(map[string]interface{}{})
|
||||
assert.Len(t, keys, 0)
|
||||
}
|
||||
|
||||
// --- WebhookDelivery struct tests ---
|
||||
|
||||
func TestWebhookDelivery_JSON(t *testing.T) {
|
||||
delivery := WebhookDelivery{
|
||||
HookID: 1,
|
||||
Status: "success",
|
||||
Attempts: 1,
|
||||
Payload: map[string]interface{}{"event": "test"},
|
||||
}
|
||||
data, err := json.Marshal(delivery)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(data), `"hook_id":1`)
|
||||
assert.Contains(t, string(data), `"status":"success"`)
|
||||
|
||||
var decoded WebhookDelivery
|
||||
err = json.Unmarshal(data, &decoded)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, uint(1), decoded.HookID)
|
||||
assert.Equal(t, "success", decoded.Status)
|
||||
}
|
||||
|
||||
// --- Test helpers ---
|
||||
// NOTE: These use in-memory SQLite DB via the repository layer.
|
||||
// For the webhook processor tests, most tests don't need a DB (processors
|
||||
// accept nil repos for logging-only processing). Only the full integration
|
||||
// tests (ProcessWebhookEvent) need actual repos.
|
||||
|
||||
func createTestHookRepo(t *testing.T) *repository.IntegrationHookRepo {
|
||||
t.Helper()
|
||||
hookRepo, _ := setupIntegrationHookTestDB(t)
|
||||
return hookRepo
|
||||
}
|
||||
|
||||
func createTestAppRepo(t *testing.T) *repository.IntegrationAppRepo {
|
||||
t.Helper()
|
||||
_, appRepo := setupIntegrationHookTestDB(t)
|
||||
return appRepo
|
||||
}
|
||||
Reference in New Issue
Block a user