660 lines
22 KiB
Go
660 lines
22 KiB
Go
package line
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
channelpkg "github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
// ============================================================
|
|
// LineService tests
|
|
// ============================================================
|
|
|
|
func TestNewLineService(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
assert.NotNil(t, svc)
|
|
assert.NotNil(t, svc.client)
|
|
}
|
|
|
|
func TestLineService_ReplyMessage(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTimeout(2 * time.Second)
|
|
svc.client.SetRetryCount(0)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
assert.Equal(t, "Bearer test-token", req.Header.Get("Authorization"))
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
err := svc.ReplyMessage(context.Background(), "test-token", "reply-token", []OutboundMsg{{Type: "text", Text: "hello"}})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestLineService_ReplyMessage_Error(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
// The service uses real HTTP — the default URL won't resolve, but resty may retry
|
|
// Let's test the error path by using an invalid token that returns non-200
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 401,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
err := svc.ReplyMessage(context.Background(), "bad-token", "reply-token", []OutboundMsg{{Type: "text", Text: "hello"}})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 401")
|
|
}
|
|
|
|
func TestLineService_PushMessage(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
resp, err := svc.PushMessage(context.Background(), "token", "user123", []OutboundMsg{{Type: "text", Text: "hello"}})
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, resp)
|
|
}
|
|
|
|
func TestLineService_PushMessage_Error(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 401,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
_, err := svc.PushMessage(context.Background(), "bad-token", "user123", []OutboundMsg{{Type: "text", Text: "hello"}})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 401")
|
|
}
|
|
|
|
func TestLineService_GetUserProfile(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
body := `{"userId":"U123","displayName":"Test User","pictureUrl":"http://example.com/pic.jpg","statusMessage":"Hello"}`
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: stringReadCloser(body),
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
}, nil
|
|
}})
|
|
|
|
profile, err := svc.GetUserProfile(context.Background(), "token", "U123")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "U123", profile.UserID)
|
|
assert.Equal(t, "Test User", profile.DisplayName)
|
|
assert.Equal(t, "http://example.com/pic.jpg", profile.PictureURL)
|
|
assert.Equal(t, "Hello", profile.StatusMessage)
|
|
}
|
|
|
|
func TestLineService_GetUserProfile_Error(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 404,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
_, err := svc.GetUserProfile(context.Background(), "token", "invalid")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 404")
|
|
}
|
|
|
|
func TestLineService_VerifySignature(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
|
|
// Generate a valid signature for "test-body" with "secret"
|
|
body := "test-body"
|
|
secret := "test-secret"
|
|
|
|
// Compute expected signature
|
|
sig := computeHMACBase64(secret, body)
|
|
|
|
assert.True(t, svc.VerifySignature(secret, body, sig))
|
|
assert.False(t, svc.VerifySignature(secret, body, "wrong-sig"))
|
|
assert.False(t, svc.VerifySignature("wrong-secret", body, sig))
|
|
}
|
|
|
|
func TestLineService_ValidateAccessToken(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
err := svc.ValidateAccessToken(context.Background(), "valid-token")
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestLineService_ValidateAccessToken_Invalid(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: 401,
|
|
Body: http.NoBody,
|
|
Header: make(http.Header),
|
|
}, nil
|
|
}})
|
|
|
|
err := svc.ValidateAccessToken(context.Background(), "invalid-token")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 401")
|
|
}
|
|
|
|
// ============================================================
|
|
// LineProvider tests
|
|
// ============================================================
|
|
|
|
func TestNewLineProvider(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
pipe := NewIncomingProcessor(svc)
|
|
provider := NewLineProvider(svc, &Repository{}, pipe)
|
|
assert.NotNil(t, provider)
|
|
}
|
|
|
|
func TestLineProvider_Identity(t *testing.T) {
|
|
provider := NewLineProvider(NewLineService(nil), &Repository{}, NewIncomingProcessor(NewLineService(nil)))
|
|
assert.Equal(t, channelpkg.ChannelLine, provider.Type())
|
|
assert.Equal(t, "LINE", provider.Name())
|
|
assert.NotEmpty(t, provider.Description())
|
|
}
|
|
|
|
func TestLineProvider_ConfigSchema(t *testing.T) {
|
|
provider := NewLineProvider(NewLineService(nil), &Repository{}, NewIncomingProcessor(NewLineService(nil)))
|
|
schema := provider.ConfigSchema()
|
|
require.NotNil(t, schema)
|
|
assert.Equal(t, "object", schema.Type)
|
|
assert.Contains(t, schema.Required, "channel_access_token")
|
|
assert.Contains(t, schema.Required, "channel_secret")
|
|
}
|
|
|
|
func TestLineProvider_ValidateConfig(t *testing.T) {
|
|
provider := NewLineProvider(NewLineService(nil), &Repository{}, NewIncomingProcessor(NewLineService(nil)))
|
|
ctx := context.Background()
|
|
|
|
t.Run("missing token", func(t *testing.T) {
|
|
err := provider.ValidateConfig(ctx, channelpkg.ChannelConfig{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "channel_access_token")
|
|
})
|
|
|
|
t.Run("missing secret", func(t *testing.T) {
|
|
err := provider.ValidateConfig(ctx, channelpkg.ChannelConfig{"channel_access_token": "token"})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "channel_secret")
|
|
})
|
|
|
|
t.Run("valid config", func(t *testing.T) {
|
|
err := provider.ValidateConfig(ctx, channelpkg.ChannelConfig{
|
|
"channel_access_token": "token",
|
|
"channel_secret": "secret",
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestLineProvider_DefaultConfig(t *testing.T) {
|
|
provider := NewLineProvider(NewLineService(nil), &Repository{}, NewIncomingProcessor(NewLineService(nil)))
|
|
config := provider.DefaultConfig()
|
|
assert.NotNil(t, config)
|
|
assert.Contains(t, config, "channel_access_token")
|
|
assert.Contains(t, config, "channel_secret")
|
|
}
|
|
|
|
func TestLineProvider_OnCreate(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
// Make ValidateAccessToken succeed
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: 200, Body: http.NoBody, Header: make(http.Header)}, nil
|
|
}})
|
|
provider := NewLineProvider(svc, &Repository{}, NewIncomingProcessor(svc))
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
config := channelpkg.ChannelConfig{"channel_access_token": "token"}
|
|
result, err := provider.OnCreate(context.Background(), inbox, config)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
}
|
|
|
|
func TestLineProvider_OnDestroy(t *testing.T) {
|
|
provider := NewLineProvider(NewLineService(nil), &Repository{}, NewIncomingProcessor(NewLineService(nil)))
|
|
err := provider.OnDestroy(context.Background(), &model.Inbox{}, channelpkg.ChannelConfig{})
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestLineProvider_ProcessIncoming(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
pipe := NewIncomingProcessor(svc)
|
|
provider := NewLineProvider(svc, &Repository{}, pipe)
|
|
ctx := context.Background()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}
|
|
|
|
t.Run("valid text message", func(t *testing.T) {
|
|
event := WebhookEvent{
|
|
Events: []EventObject{
|
|
{
|
|
Type: "message",
|
|
ReplyToken: "rt-123",
|
|
Source: EventSource{Type: "user", UserID: "U123"},
|
|
Message: json.RawMessage(`{"type":"text","id":"msg-1","text":"Hello LINE"}`),
|
|
},
|
|
},
|
|
}
|
|
payload, _ := json.Marshal(event)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, msg)
|
|
assert.Equal(t, "msg-1", msg.SourceID)
|
|
assert.Equal(t, "U123", msg.SenderID)
|
|
assert.Equal(t, "Hello LINE", msg.Content)
|
|
assert.Equal(t, channelpkg.ContentText, msg.ContentType)
|
|
assert.Equal(t, "rt-123", msg.Extra["reply_token"])
|
|
})
|
|
|
|
t.Run("no events", func(t *testing.T) {
|
|
payload, _ := json.Marshal(WebhookEvent{Events: []EventObject{}})
|
|
_, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no events")
|
|
})
|
|
|
|
t.Run("invalid JSON", func(t *testing.T) {
|
|
_, err := provider.ProcessIncoming(ctx, inbox, []byte("invalid"))
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("follow event", func(t *testing.T) {
|
|
event := WebhookEvent{
|
|
Events: []EventObject{
|
|
{Type: "follow", Source: EventSource{Type: "user", UserID: "U456"}},
|
|
},
|
|
}
|
|
payload, _ := json.Marshal(event)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, msg) // follow events return nil
|
|
})
|
|
|
|
t.Run("unfollow event", func(t *testing.T) {
|
|
event := WebhookEvent{
|
|
Events: []EventObject{
|
|
{Type: "unfollow", Source: EventSource{Type: "user", UserID: "U789"}},
|
|
},
|
|
}
|
|
payload, _ := json.Marshal(event)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, msg)
|
|
})
|
|
|
|
t.Run("postback event", func(t *testing.T) {
|
|
event := WebhookEvent{
|
|
Events: []EventObject{
|
|
{
|
|
Type: "postback",
|
|
Source: EventSource{Type: "user", UserID: "U000"},
|
|
Postback: &PostbackData{Data: "action=buy"},
|
|
},
|
|
},
|
|
}
|
|
payload, _ := json.Marshal(event)
|
|
msg, err := provider.ProcessIncoming(ctx, inbox, payload)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, msg)
|
|
assert.Equal(t, "action=buy", msg.Content)
|
|
})
|
|
}
|
|
|
|
func TestLineProvider_ValidateWebhookRequest(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
provider := NewLineProvider(svc, &Repository{}, NewIncomingProcessor(svc))
|
|
ctx := context.Background()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
|
|
t.Run("missing secret", func(t *testing.T) {
|
|
err := provider.ValidateWebhookRequest(ctx, inbox, &channelpkg.WebhookRequest{
|
|
Headers: map[string]string{"X-Line-Signature": "sig"},
|
|
Body: []byte("body"),
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "channel_secret")
|
|
})
|
|
|
|
t.Run("missing signature", func(t *testing.T) {
|
|
inboxWithConfig := &model.Inbox{
|
|
Base: model.Base{ID: 1},
|
|
ChannelConfig: `{"channel_secret":"secret"}`,
|
|
}
|
|
err := provider.ValidateWebhookRequest(ctx, inboxWithConfig, &channelpkg.WebhookRequest{
|
|
Headers: map[string]string{},
|
|
Body: []byte("body"),
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "missing X-Line-Signature")
|
|
})
|
|
|
|
t.Run("invalid signature", func(t *testing.T) {
|
|
inboxWithConfig := &model.Inbox{
|
|
Base: model.Base{ID: 1},
|
|
ChannelConfig: `{"channel_secret":"secret"}`,
|
|
}
|
|
err := provider.ValidateWebhookRequest(ctx, inboxWithConfig, &channelpkg.WebhookRequest{
|
|
Headers: map[string]string{"X-Line-Signature": "wrong-sig"},
|
|
Body: []byte("body"),
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid HMAC")
|
|
})
|
|
|
|
t.Run("valid signature", func(t *testing.T) {
|
|
body := "test-body"
|
|
secret := "test-secret"
|
|
sig := computeHMACBase64(secret, body)
|
|
inboxWithConfig := &model.Inbox{
|
|
Base: model.Base{ID: 1},
|
|
ChannelConfig: `{"channel_secret":"test-secret"}`,
|
|
}
|
|
err := provider.ValidateWebhookRequest(ctx, inboxWithConfig, &channelpkg.WebhookRequest{
|
|
Headers: map[string]string{"X-Line-Signature": sig},
|
|
Body: []byte(body),
|
|
})
|
|
assert.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestLineProvider_SendMessage(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{StatusCode: 200, Body: http.NoBody, Header: make(http.Header)}, nil
|
|
}})
|
|
provider := NewLineProvider(svc, &Repository{}, NewIncomingProcessor(svc))
|
|
ctx := context.Background()
|
|
inbox := &model.Inbox{
|
|
Base: model.Base{ID: 1},
|
|
ChannelConfig: `{"channel_access_token":"token"}`,
|
|
}
|
|
|
|
t.Run("no contact source_id", func(t *testing.T) {
|
|
contact := &model.Contact{}
|
|
msg := &model.Message{Content: "test"}
|
|
_, err := provider.SendMessage(ctx, inbox, msg, contact)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("valid send", func(t *testing.T) {
|
|
contact := &model.Contact{SourceID: "U123"}
|
|
msg := &model.Message{Content: "Hello"}
|
|
result, err := provider.SendMessage(ctx, inbox, msg, contact)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
})
|
|
}
|
|
|
|
func TestLineProvider_GetContactProfile(t *testing.T) {
|
|
svc := NewLineService(nil)
|
|
svc.client.SetTransport(&testTransport{handler: func(req *http.Request) (*http.Response, error) {
|
|
body := `{"userId":"U123","displayName":"Test User","pictureUrl":"http://pic.jpg","statusMessage":"Hi"}`
|
|
return &http.Response{
|
|
StatusCode: 200,
|
|
Body: stringReadCloser(body),
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
}, nil
|
|
}})
|
|
provider := NewLineProvider(svc, &Repository{}, NewIncomingProcessor(svc))
|
|
inbox := &model.Inbox{
|
|
Base: model.Base{ID: 1},
|
|
ChannelConfig: `{"channel_access_token":"token"}`,
|
|
}
|
|
|
|
profile, err := provider.GetContactProfile(context.Background(), inbox, "U123")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Test User", profile.Name)
|
|
assert.Equal(t, "http://pic.jpg", profile.AvatarURL)
|
|
assert.Equal(t, "Hi", profile.Extra["status_message"])
|
|
}
|
|
|
|
func TestLineProvider_Capabilities(t *testing.T) {
|
|
provider := NewLineProvider(NewLineService(nil), &Repository{}, NewIncomingProcessor(NewLineService(nil)))
|
|
caps := provider.Capabilities()
|
|
assert.True(t, caps.SupportsAttachments)
|
|
assert.True(t, caps.SupportsLocation)
|
|
assert.True(t, caps.SupportsReplies)
|
|
assert.True(t, caps.SupportsVoiceMessages)
|
|
assert.True(t, caps.SupportsCustomCards)
|
|
assert.True(t, caps.SupportsTemplates)
|
|
assert.Equal(t, int64(10*1024*1024), caps.MaxAttachmentSize)
|
|
assert.Equal(t, 5000, caps.MaxTextLength)
|
|
}
|
|
|
|
// ============================================================
|
|
// Pipeline tests
|
|
// ============================================================
|
|
|
|
func TestNewIncomingProcessor(t *testing.T) {
|
|
p := NewIncomingProcessor(NewLineService(nil))
|
|
assert.NotNil(t, p)
|
|
}
|
|
|
|
func TestIncomingProcessor_ProcessEvent_NoSourceID(t *testing.T) {
|
|
p := NewIncomingProcessor(NewLineService(nil))
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
_, err := p.ProcessEvent(context.Background(), inbox, EventObject{})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no source ID")
|
|
}
|
|
|
|
func TestIncomingProcessor_ProcessEvent_MessageTypes(t *testing.T) {
|
|
p := NewIncomingProcessor(NewLineService(nil))
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}
|
|
|
|
t.Run("text message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"text","id":"m1","text":"hello"}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hello", msg.Content)
|
|
assert.Equal(t, channelpkg.ContentText, msg.ContentType)
|
|
})
|
|
|
|
t.Run("image message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"image","id":"m2","originalContentUrl":"http://img.jpg"}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, channelpkg.ContentImage, msg.ContentType)
|
|
assert.Len(t, msg.Attachments, 1)
|
|
})
|
|
|
|
t.Run("video message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"video","id":"m3","originalContentUrl":"http://vid.mp4"}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, channelpkg.ContentVideo, msg.ContentType)
|
|
})
|
|
|
|
t.Run("audio message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"audio","id":"m4","originalContentUrl":"http://au.mp3","duration":5000}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, channelpkg.ContentAudio, msg.ContentType)
|
|
assert.Contains(t, msg.Content, "Audio message")
|
|
})
|
|
|
|
t.Run("file message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"file","id":"m5","fileName":"doc.pdf","fileSize":1024,"originalContentUrl":"http://file.pdf"}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, channelpkg.ContentFile, msg.ContentType)
|
|
assert.Equal(t, "doc.pdf", msg.Content)
|
|
})
|
|
|
|
t.Run("location message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"location","id":"m6","title":"Office","address":"123 Main St","latitude":35.6762,"longitude":139.6503}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, channelpkg.ContentLocation, msg.ContentType)
|
|
assert.Contains(t, msg.Content, "Office")
|
|
})
|
|
|
|
t.Run("sticker message", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"sticker","id":"m7","packageId":"1","stickerId":"100"}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, msg.Content, "Sticker")
|
|
})
|
|
|
|
t.Run("unknown message type", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`{"type":"unknown","id":"m8"}`),
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, msg.Content, "Unsupported")
|
|
})
|
|
|
|
t.Run("invalid message JSON", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "message",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
Message: json.RawMessage(`invalid`),
|
|
}
|
|
_, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
assert.Error(t, err)
|
|
})
|
|
|
|
t.Run("unhandled event type", func(t *testing.T) {
|
|
event := EventObject{
|
|
Type: "beacon",
|
|
Source: EventSource{Type: "user", UserID: "U1"},
|
|
}
|
|
msg, err := p.ProcessEvent(context.Background(), inbox, event)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, msg)
|
|
})
|
|
}
|
|
|
|
func TestResolveSourceID(t *testing.T) {
|
|
assert.Equal(t, "U123", resolveSourceID(EventSource{Type: "user", UserID: "U123"}))
|
|
assert.Equal(t, "G456", resolveSourceID(EventSource{Type: "group", GroupID: "G456"}))
|
|
assert.Equal(t, "R789", resolveSourceID(EventSource{Type: "room", RoomID: "R789"}))
|
|
assert.Equal(t, "", resolveSourceID(EventSource{}))
|
|
}
|
|
|
|
func TestNewOutgoingProcessor(t *testing.T) {
|
|
p := NewOutgoingProcessor(NewLineService(nil))
|
|
assert.NotNil(t, p)
|
|
}
|
|
|
|
func TestOutgoingProcessor_SendMessage_NoSourceID(t *testing.T) {
|
|
p := NewOutgoingProcessor(NewLineService(nil))
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
msg := &model.Message{Content: "test"}
|
|
contact := &model.Contact{}
|
|
_, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestNewEventBridge(t *testing.T) {
|
|
b := NewEventBridge(NewOutgoingProcessor(NewLineService(nil)))
|
|
assert.NotNil(t, b)
|
|
}
|
|
|
|
// ============================================================
|
|
// Helper functions tests
|
|
// ============================================================
|
|
|
|
func TestParseInboxConfig(t *testing.T) {
|
|
assert.Empty(t, parseInboxConfig(""))
|
|
assert.NotEmpty(t, parseInboxConfig(`{"key":"val"}`))
|
|
assert.Empty(t, parseInboxConfig("invalid"))
|
|
}
|
|
|
|
func TestConfigStr(t *testing.T) {
|
|
assert.Equal(t, "default", configStr(channelpkg.ChannelConfig{}, "key", "default"))
|
|
assert.Equal(t, "val", configStr(channelpkg.ChannelConfig{"key": "val"}, "key", "default"))
|
|
assert.Equal(t, "default", configStr(channelpkg.ChannelConfig{"key": ""}, "key", "default"))
|
|
}
|
|
|
|
// ============================================================
|
|
// Repository tests — require a DB; skip nil DB tests to avoid panics
|
|
// ============================================================
|
|
|
|
// ============================================================
|
|
// Test helpers
|
|
// ============================================================
|
|
|
|
type testTransport struct {
|
|
handler func(*http.Request) (*http.Response, error)
|
|
}
|
|
|
|
func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return t.handler(req)
|
|
}
|
|
|
|
func stringReadCloser(s string) io.ReadCloser {
|
|
return io.NopCloser(strings.NewReader(s))
|
|
}
|
|
|
|
func computeHMACBase64(secret, body string) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write([]byte(body))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|