Files
ragflow/internal/channels/line_test.go
Haruko386 8a66aa7aae feat[Go]: add line chat bot for chat channel (#17594)
### Summary

As title, this can not be tested for now
Close #17520

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-31 16:55:05 +08:00

143 lines
4.8 KiB
Go

//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package channels
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"ragflow/internal/channels/core"
)
func TestNewLineChannelFromConfigRequiresCredentials(t *testing.T) {
if _, err := newLineChannelFromConfig("account-1", map[string]any{"channel_secret": "secret"}); err == nil {
t.Fatal("newLineChannelFromConfig succeeded without channel_access_token")
}
}
func TestNewLineChannelFromConfigNormalizesConfig(t *testing.T) {
ch, err := newLineChannelFromConfig("account-1", map[string]any{
"channel_secret": "secret-1",
"channel_access_token": "token-1",
"webhook_host": "127.0.0.1",
"webhook_port": "3011",
"timeout_secs": "7",
})
if err != nil {
t.Fatalf("newLineChannelFromConfig returned error: %v", err)
}
if ch.account.ChannelSecret != "secret-1" {
t.Fatalf("channel secret = %q, want secret-1", ch.account.ChannelSecret)
}
if ch.account.ChannelAccessToken != "token-1" {
t.Fatalf("channel access token = %q, want token-1", ch.account.ChannelAccessToken)
}
if ch.account.WebhookHost != "127.0.0.1" {
t.Fatalf("webhook host = %q, want 127.0.0.1", ch.account.WebhookHost)
}
if ch.account.WebhookPort != 3011 {
t.Fatalf("webhook port = %d, want 3011", ch.account.WebhookPort)
}
if ch.account.Timeout != 7*time.Second {
t.Fatalf("timeout = %s, want 7s", ch.account.Timeout)
}
}
func TestLineWebhookRejectsBadSignature(t *testing.T) {
ch := newLineChannel(lineAccount{AccountID: "account-1", ChannelSecret: "secret", ChannelAccessToken: "token"})
server := &lineWebhookServer{channels: map[string]*lineChannel{"account-1": ch}}
req := httptest.NewRequest(http.MethodPost, "/line/account-1/webhook", strings.NewReader(`{"events":[]}`))
req.Header.Set("X-Line-Signature", "bad")
rec := httptest.NewRecorder()
server.handleRequest(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
}
}
func TestLineWebhookDispatchesTextMessage(t *testing.T) {
body := `{"events":[{"type":"message","replyToken":"reply-1","source":{"type":"user","userId":"user-1"},"message":{"id":"message-1","type":"text","text":"hello"}}]}`
ch := newLineChannel(lineAccount{AccountID: "account-1", ChannelSecret: "secret", ChannelAccessToken: "token"})
server := &lineWebhookServer{channels: map[string]*lineChannel{"account-1": ch}}
var got core.IncomingMessage
handled := make(chan struct{}, 1)
ch.SetMessageHandler(func(ctx context.Context, msg core.IncomingMessage) error {
got = msg
handled <- struct{}{}
return nil
})
req := httptest.NewRequest(http.MethodPost, "/line/account-1/webhook", strings.NewReader(body))
req.Header.Set("X-Line-Signature", lineSignature("secret", body))
rec := httptest.NewRecorder()
server.handleRequest(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
}
select {
case <-handled:
case <-time.After(time.Second):
t.Fatal("message handler was not invoked")
}
if got.Channel != "line" || got.AccountID != "account-1" || got.ChatID != "user-1" || got.ChatType != "p2p" || got.MessageID != "message-1" || got.Text != "hello" {
t.Fatalf("incoming message = %#v", got)
}
if token := ch.takeReplyToken("message-1"); token != "reply-1" {
t.Fatalf("reply token = %q, want reply-1", token)
}
if token := ch.takeReplyToken("message-1"); token != "" {
t.Fatalf("reply token was not single-use: %q", token)
}
}
func TestLineEnqueueDoesNotMarkDroppedMessageSeen(t *testing.T) {
ch := newLineChannel(lineAccount{AccountID: "account-1", ChannelSecret: "secret", ChannelAccessToken: "token"})
worker := &lineWorker{queue: make(chan core.IncomingMessage, 1)}
worker.queue <- core.IncomingMessage{MessageID: "queued"}
ch.workers["chat-1"] = worker
ok := ch.enqueueIncoming(core.IncomingMessage{
ChatID: "chat-1",
MessageID: "dropped",
})
if ok {
t.Fatal("enqueueIncoming succeeded for a full queue")
}
if _, seen := ch.seen["dropped"]; seen {
t.Fatal("dropped message was marked seen")
}
}
func lineSignature(secret, body string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(body))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}