2026-04-17 12:40:47 +08:00
|
|
|
package utils
|
2026-04-17 11:53:25 +08:00
|
|
|
|
|
|
|
|
import (
|
2026-08-21 00:41:07 +08:00
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/config"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
2026-04-17 11:53:25 +08:00
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-04-17 12:50:06 +08:00
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/glebarez/sqlite"
|
|
|
|
|
"github.com/mlogclub/simple/sqls"
|
|
|
|
|
"gorm.io/gorm"
|
2026-04-17 11:53:25 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestBuildIMMessageAssetPayloadForResponseAddsSignedURL(t *testing.T) {
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-28 22:23:13 +08:00
|
|
|
payload := `{"asset_id":"asset_1","provider":"local","storage_key":"attachments/demo.png","filename":"demo.png"}`
|
2026-04-17 11:53:25 +08:00
|
|
|
got := buildIMMessageAssetPayloadForResponse(payload)
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(got, `"provider":"local"`) {
|
|
|
|
|
t.Fatalf("expected provider in payload, got: %s", got)
|
|
|
|
|
}
|
2026-08-28 22:23:13 +08:00
|
|
|
if !strings.Contains(got, `"storage_key":"attachments/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected storage_key in payload, got: %s", got)
|
2026-04-17 11:53:25 +08:00
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, `"url":"https://files.example.com/attachments/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected signed url in payload, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestSanitizeMessageHTMLStripsStoredSrcForManagedImages(t *testing.T) {
|
|
|
|
|
html := `<p><img src="https://files.example.com/demo.png" data-provider="local" data-storage-key="attachments/demo.png" alt="demo"></p>`
|
|
|
|
|
|
2026-04-17 12:40:47 +08:00
|
|
|
got := SanitizeMessageHTML(html)
|
2026-04-17 11:53:25 +08:00
|
|
|
|
|
|
|
|
if strings.Contains(got, `src=`) {
|
|
|
|
|
t.Fatalf("expected src removed from stored html, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, `data-provider="local"`) {
|
|
|
|
|
t.Fatalf("expected data-provider kept, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, `data-storage-key="attachments/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected data-storage-key kept, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildMessageHTMLForResponseAddsSignedURL(t *testing.T) {
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
html := `<p><img data-provider="local" data-storage-key="attachments/demo.png" alt="demo"></p>`
|
2026-04-17 12:40:47 +08:00
|
|
|
got := BuildMessageHTMLForResponse(html)
|
2026-04-17 11:53:25 +08:00
|
|
|
|
|
|
|
|
if !strings.Contains(got, `src="https://files.example.com/attachments/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected signed src in response html, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-18 22:24:30 +08:00
|
|
|
func TestBuildRuntimeMessageTextForHTML(t *testing.T) {
|
|
|
|
|
got := BuildRuntimeMessageText(enums.IMMessageTypeHTML, `<p>你好</p><p><img data-provider="local" data-storage-key="images/demo.png" alt="demo"></p>`)
|
|
|
|
|
if got != "你好 [图片]" {
|
|
|
|
|
t.Fatalf("expected html converted to plain text summary, got: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestBuildRuntimeMessageTextForAssetMessages(t *testing.T) {
|
|
|
|
|
if got := BuildRuntimeMessageText(enums.IMMessageTypeImage, "demo.png"); got != "[图片] demo.png" {
|
|
|
|
|
t.Fatalf("unexpected image runtime text: %q", got)
|
|
|
|
|
}
|
|
|
|
|
if got := BuildRuntimeMessageText(enums.IMMessageTypeAttachment, "spec.pdf"); got != "[附件] spec.pdf" {
|
|
|
|
|
t.Fatalf("unexpected attachment runtime text: %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 11:53:25 +08:00
|
|
|
func TestBuildRenderableMessageTransformsPayloadAndHTML(t *testing.T) {
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
image := &models.Message{
|
|
|
|
|
MessageType: enums.IMMessageTypeImage,
|
2026-08-28 22:23:13 +08:00
|
|
|
Payload: `{"asset_id":"asset_1","provider":"local","storage_key":"attachments/demo.png","filename":"demo.png"}`,
|
2026-04-17 11:53:25 +08:00
|
|
|
}
|
|
|
|
|
_, imagePayload := BuildRenderableMessage(image)
|
|
|
|
|
if !strings.Contains(imagePayload, `"url":"https://files.example.com/attachments/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected image payload signed url, got: %s", imagePayload)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
htmlMsg := &models.Message{
|
|
|
|
|
MessageType: enums.IMMessageTypeHTML,
|
|
|
|
|
Content: `<p><img data-provider="local" data-storage-key="attachments/demo.png"></p>`,
|
|
|
|
|
}
|
|
|
|
|
htmlContent, _ := BuildRenderableMessage(htmlMsg)
|
|
|
|
|
if !strings.Contains(htmlContent, `src="https://files.example.com/attachments/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected html content signed src, got: %s", htmlContent)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-17 12:50:06 +08:00
|
|
|
|
2026-04-17 13:35:00 +08:00
|
|
|
func TestNormalizeMessageHTMLAssetsKeepsValidAttrsAndRemovesSrc(t *testing.T) {
|
2026-04-17 12:50:06 +08:00
|
|
|
setupMessageTestDB(t)
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
createTestAsset(t, &models.Asset{
|
|
|
|
|
AssetID: "asset_local_1",
|
|
|
|
|
Provider: enums.AssetProviderLocal,
|
|
|
|
|
StorageKey: "images/demo.png",
|
|
|
|
|
Filename: "demo.png",
|
|
|
|
|
FileSize: 123,
|
|
|
|
|
MimeType: "image/png",
|
|
|
|
|
Status: enums.AssetStatusSuccess,
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-17 13:35:00 +08:00
|
|
|
got, err := NormalizeMessageHTMLAssets(`<p><img src="https://files.example.com/images/demo.png" data-asset-id="asset_local_1" data-provider="local" data-storage-key="images/demo.png" alt="demo"></p>`)
|
2026-04-17 13:34:36 +08:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("expected normalization success, got error: %v", err)
|
|
|
|
|
}
|
2026-04-17 12:50:06 +08:00
|
|
|
|
|
|
|
|
if !strings.Contains(got, `data-asset-id="asset_local_1"`) {
|
|
|
|
|
t.Fatalf("expected data-asset-id added, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, `data-provider="local"`) {
|
|
|
|
|
t.Fatalf("expected data-provider added, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(got, `data-storage-key="images/demo.png"`) {
|
|
|
|
|
t.Fatalf("expected data-storage-key added, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(got, `src=`) {
|
|
|
|
|
t.Fatalf("expected src removed after asset binding, got: %s", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 13:35:00 +08:00
|
|
|
func TestNormalizeMessageHTMLAssetsRejectsMissingAssetMetadata(t *testing.T) {
|
2026-04-17 12:50:06 +08:00
|
|
|
setupMessageTestDB(t)
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-17 13:34:36 +08:00
|
|
|
_, err := NormalizeMessageHTMLAssets(`<p><img src="https://unknown.example.com/demo.png" alt="demo"></p>`)
|
|
|
|
|
if err == nil {
|
2026-04-17 13:35:00 +08:00
|
|
|
t.Fatalf("expected missing image asset metadata rejected")
|
2026-04-17 13:34:36 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeMessageHTMLAssetsRejectsIncompleteAttrs(t *testing.T) {
|
|
|
|
|
setupMessageTestDB(t)
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-04-17 12:50:06 +08:00
|
|
|
|
2026-04-17 13:34:36 +08:00
|
|
|
_, err := NormalizeMessageHTMLAssets(`<p><img data-asset-id="asset1" data-provider="local" alt="demo"></p>`)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("expected incomplete asset attrs rejected")
|
2026-04-17 12:50:06 +08:00
|
|
|
}
|
2026-04-17 13:34:36 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeMessageHTMLAssetsRejectsMismatchedAttrs(t *testing.T) {
|
|
|
|
|
setupMessageTestDB(t)
|
|
|
|
|
config.SetCurrent(&config.Config{
|
|
|
|
|
Storage: config.StorageConfig{
|
|
|
|
|
Default: enums.AssetProviderLocal,
|
|
|
|
|
Local: config.LocalStorageConfig{
|
|
|
|
|
BaseURL: "https://files.example.com",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
createTestAsset(t, &models.Asset{
|
|
|
|
|
AssetID: "asset_local_2",
|
|
|
|
|
Provider: enums.AssetProviderLocal,
|
|
|
|
|
StorageKey: "images/real.png",
|
|
|
|
|
Filename: "real.png",
|
|
|
|
|
FileSize: 456,
|
|
|
|
|
MimeType: "image/png",
|
|
|
|
|
Status: enums.AssetStatusSuccess,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
_, err := NormalizeMessageHTMLAssets(`<p><img data-asset-id="asset_local_2" data-provider="local" data-storage-key="images/wrong.png" alt="demo"></p>`)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("expected mismatched asset attrs rejected")
|
2026-04-17 12:50:06 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setupMessageTestDB(t *testing.T) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("open sqlite failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := db.AutoMigrate(&models.Asset{}); err != nil {
|
|
|
|
|
t.Fatalf("auto migrate asset failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
sqls.SetDB(db)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createTestAsset(t *testing.T, item *models.Asset) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
now := time.Now()
|
|
|
|
|
if item.CreatedAt.IsZero() {
|
|
|
|
|
item.CreatedAt = now
|
|
|
|
|
}
|
|
|
|
|
if item.UpdatedAt.IsZero() {
|
|
|
|
|
item.UpdatedAt = now
|
|
|
|
|
}
|
|
|
|
|
if err := sqls.DB().Create(item).Error; err != nil {
|
|
|
|
|
t.Fatalf("create asset failed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|