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.
182 lines
5.7 KiB
Plaintext
182 lines
5.7 KiB
Plaintext
package service
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// setupPushDeliveryService 创建 PushTokenRepo + PushDeliveryService 测试实例。
|
|
func setupPushDeliveryService(t *testing.T) (*gorm.DB, *repository.PushTokenRepo, *PushDeliveryService) {
|
|
t.Helper()
|
|
db := setupServiceTestDB(t)
|
|
repo := repository.NewPushTokenRepo(db)
|
|
svc := NewPushDeliveryService(repo, "test-vapid-public", "test-vapid-private", "mailto:test@example.com")
|
|
return db, repo, svc
|
|
}
|
|
|
|
// setupWebhookDeliveryService 创建 WebhookSubscriptionRepo + WebhookDeliveryService 测试实例。
|
|
func setupWebhookDeliveryService(t *testing.T) (*gorm.DB, *repository.WebhookSubscriptionRepo, *WebhookDeliveryService) {
|
|
t.Helper()
|
|
db := setupServiceTestDB(t)
|
|
repo := repository.NewWebhookSubscriptionRepo(db)
|
|
svc := NewWebhookDeliveryService(repo)
|
|
return db, repo, svc
|
|
}
|
|
|
|
// ========== PushDeliveryService.SendPushNotification ==========
|
|
|
|
func TestPushDeliveryService_SendPushNotification_成功(t *testing.T) {
|
|
db, _, svc := setupPushDeliveryService(t)
|
|
account := createTestAccount(t, db)
|
|
user := createTestUser(t, db, account.ID)
|
|
|
|
// 创建推送令牌
|
|
createTestPushToken(t, db, user.ID, "push-token-ios-001", "ios")
|
|
createTestPushToken(t, db, user.ID, "push-token-android-002", "android")
|
|
|
|
payload := PushPayload{
|
|
Title: "测试推送",
|
|
Body: "这是一条测试推送消息",
|
|
Data: map[string]interface{}{"conversation_id": 123},
|
|
}
|
|
|
|
err := svc.SendPushNotification(context.Background(), user.ID, payload)
|
|
require.NoError(t, err)
|
|
// 当前实现仅记录日志,不返回错误
|
|
}
|
|
|
|
func TestPushDeliveryService_SendPushNotification_无令牌(t *testing.T) {
|
|
db, _, svc := setupPushDeliveryService(t)
|
|
account := createTestAccount(t, db)
|
|
user := createTestUser(t, db, account.ID)
|
|
|
|
payload := PushPayload{
|
|
Title: "测试推送",
|
|
Body: "无令牌推送",
|
|
}
|
|
|
|
err := svc.SendPushNotification(context.Background(), user.ID, payload)
|
|
require.NoError(t, err)
|
|
// 无令牌时跳过推送,不返回错误
|
|
}
|
|
|
|
// ========== WebhookDeliveryService.DeliverEvent ==========
|
|
|
|
func TestWebhookDeliveryService_DeliverEvent_成功(t *testing.T) {
|
|
db, _, svc := setupWebhookDeliveryService(t)
|
|
account := createTestAccount(t, db)
|
|
|
|
// 使用 httptest 创建模拟 webhook 端点
|
|
var receivedRequest struct {
|
|
body []byte
|
|
signature string
|
|
event string
|
|
deliveryID string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
receivedRequest.mu.Lock()
|
|
defer receivedRequest.mu.Unlock()
|
|
|
|
bodyBytes, _ := io.ReadAll(r.Body)
|
|
receivedRequest.body = bodyBytes
|
|
receivedRequest.signature = r.Header.Get("X-Webhook-Signature")
|
|
receivedRequest.event = r.Header.Get("X-Webhook-Event")
|
|
receivedRequest.deliveryID = r.Header.Get("X-Webhook-Delivery-ID")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
t.Cleanup(func() { testServer.Close() })
|
|
|
|
// 创建 webhook 订阅指向测试服务器
|
|
eventsJSON, _ := json.Marshal([]string{"message_created"})
|
|
sub := &model.WebhookSubscription{
|
|
AccountID: account.ID,
|
|
URL: testServer.URL,
|
|
Events: eventsJSON,
|
|
Secret: "test-webhook-secret-key",
|
|
Active: true,
|
|
}
|
|
require.NoError(t, db.Create(sub).Error)
|
|
|
|
payload := map[string]interface{}{
|
|
"conversation_id": 42,
|
|
"message_id": 100,
|
|
"content": "Hello webhook",
|
|
}
|
|
|
|
err := svc.DeliverEvent(context.Background(), account.ID, "message_created", payload)
|
|
require.NoError(t, err)
|
|
|
|
// 验证测试服务器收到了正确的请求
|
|
receivedRequest.mu.Lock()
|
|
defer receivedRequest.mu.Unlock()
|
|
|
|
assert.NotEmpty(t, receivedRequest.body, "webhook 端点应收到请求体")
|
|
|
|
// 验证 X-Webhook-Signature 头
|
|
expectedSig := SignPayload(receivedRequest.body, "test-webhook-secret-key")
|
|
assert.Equal(t, expectedSig, receivedRequest.signature, "X-Webhook-Signature 应匹配 HMAC-SHA256 签名")
|
|
|
|
// 验证 X-Webhook-Event 头
|
|
assert.Equal(t, "message_created", receivedRequest.event, "X-Webhook-Event 应为 message_created")
|
|
|
|
// 验证 X-Webhook-Delivery-ID 头
|
|
assert.NotEmpty(t, receivedRequest.deliveryID, "X-Webhook-Delivery-ID 不应为空")
|
|
}
|
|
|
|
func TestWebhookDeliveryService_DeliverEvent_无订阅(t *testing.T) {
|
|
db, _, svc := setupWebhookDeliveryService(t)
|
|
account := createTestAccount(t, db)
|
|
|
|
payload := map[string]interface{}{"id": 1}
|
|
err := svc.DeliverEvent(context.Background(), account.ID, "message_created", payload)
|
|
require.NoError(t, err)
|
|
// 无匹配订阅时不发送,不返回错误
|
|
}
|
|
|
|
// ========== SignPayload ==========
|
|
|
|
func TestSignPayload_正确计算HMACSHA256(t *testing.T) {
|
|
payload := []byte(`{"event":"message_created","data":{"id":1}}`)
|
|
secret := "my-secret-key"
|
|
|
|
// 手动计算 HMAC-SHA256 用于比对
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write(payload)
|
|
expectedHex := hex.EncodeToString(mac.Sum(nil))
|
|
|
|
result := SignPayload(payload, secret)
|
|
assert.Equal(t, expectedHex, result)
|
|
}
|
|
|
|
func TestSignPayload_不同密钥产生不同签名(t *testing.T) {
|
|
payload := []byte(`{"event":"test"}`)
|
|
|
|
sig1 := SignPayload(payload, "secret-one")
|
|
sig2 := SignPayload(payload, "secret-two")
|
|
assert.NotEqual(t, sig1, sig2, "不同密钥应产生不同签名")
|
|
}
|
|
|
|
func TestSignPayload_相同密钥和载荷产生相同签名(t *testing.T) {
|
|
payload := []byte(`{"event":"consistent"}`)
|
|
secret := "same-secret"
|
|
|
|
sig1 := SignPayload(payload, secret)
|
|
sig2 := SignPayload(payload, secret)
|
|
assert.Equal(t, sig1, sig2, "相同密钥和载荷应产生相同签名")
|
|
} |