package app import ( "context" "encoding/json" "os" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" "github.com/geo-platform/tenant-api/internal/shared/auth" "github.com/geo-platform/tenant-api/internal/shared/config" "github.com/geo-platform/tenant-api/internal/shared/publicasset" "github.com/sergi/go-diff/diffmatchpatch" ) func TestUpdateEnterpriseSiteConnectionRequestTracksNullAndMissingFields(t *testing.T) { var req UpdateEnterpriseSiteConnectionRequest if err := json.Unmarshal([]byte(`{"name":"官网","default_scode":null,"secret":""}`), &req); err != nil { t.Fatalf("unmarshal request: %v", err) } if !req.Name.Set || req.Name.Value == nil || *req.Name.Value != "官网" { t.Fatalf("name patch = %#v, want set string", req.Name) } if !req.DefaultScode.Set || req.DefaultScode.Value != nil { t.Fatalf("default_scode patch = %#v, want explicit null", req.DefaultScode) } if !req.Secret.Set || req.Secret.Value == nil || *req.Secret.Value != "" { t.Fatalf("secret patch = %#v, want set empty string", req.Secret) } if req.Status.Set { t.Fatalf("status patch = %#v, want missing", req.Status) } } func TestEnterpriseSiteUpdateRealDatabaseRepro(t *testing.T) { dsn := os.Getenv("TEST_ENTERPRISE_SITE_UPDATE_DSN") if dsn == "" { t.Skip("set TEST_ENTERPRISE_SITE_UPDATE_DSN to run the real database repro") } ctx := auth.WithCurrentWorkspaceID( auth.WithActor(context.Background(), auth.Actor{UserID: 1, TenantID: 1, PrimaryWorkspaceID: 1}), 1, ) pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("connect database: %v", err) } defer pool.Close() name := "海翔" siteURL := "https://youjayou.com" appID := "admin" acode := "cn" mcode := "2" req := UpdateEnterpriseSiteConnectionRequest{ Name: EnterpriseSitePatchString{Set: true, Value: &name}, SiteURL: EnterpriseSitePatchString{Set: true, Value: &siteURL}, AppID: EnterpriseSitePatchString{Set: true, Value: &appID}, DefaultAcode: EnterpriseSitePatchString{Set: true, Value: &acode}, DefaultMcode: EnterpriseSitePatchString{Set: true, Value: &mcode}, DefaultScode: EnterpriseSitePatchString{Set: true, Value: nil}, } if _, err := NewEnterpriseSiteService(pool, config.NewStaticProvider(&config.Config{})).Update(ctx, 1, req); err != nil { t.Fatalf("update enterprise site: %v", err) } } func TestValidateEnterpriseSiteCMSURLRequiresHTTPSForRemoteWordPress(t *testing.T) { if err := validateEnterpriseSiteCMSURL("http://example.com", wordpressPlatformID); err == nil { t.Fatalf("remote wordpress http url should be rejected") } if err := validateEnterpriseSiteCMSURL("http://127.0.0.1:6060", wordpressPlatformID); err != nil { t.Fatalf("local wordpress http url should be allowed for development: %v", err) } } func TestNormalizeEnterpriseSiteURLRejectsReservedTargets(t *testing.T) { for _, raw := range []string{ "http://169.254.169.254", "http://10.0.0.8", "http://[fe80::1]", } { if _, err := normalizeEnterpriseSiteURL(raw); err == nil { t.Fatalf("reserved site url %q should be rejected", raw) } } if got, err := normalizeEnterpriseSiteURL("http://127.0.0.1:6060"); err != nil || got != "http://127.0.0.1:6060" { t.Fatalf("local loopback url = %q, %v; want allowed", got, err) } } func TestEnterpriseSiteDescriptionUsesRenderedHTMLText(t *testing.T) { got := enterpriseSiteDescription( "

2026年合肥全屋定制行业发展现状

消费者需求升级。

", "## 旧版标题\n旧版正文不应该优先进入摘要", ) if strings.Contains(got, "##") { t.Fatalf("description contains markdown heading marker: %q", got) } if strings.Contains(got, "旧版正文") { t.Fatalf("description used markdown while html text was available: %q", got) } if !strings.Contains(got, "2026年合肥全屋定制行业发展现状") { t.Fatalf("description = %q, want rendered html text", got) } } func TestEnterpriseSiteStripLeadingTitleHeadingHTMLRemovesDuplicateTitle(t *testing.T) { got := enterpriseSiteStripLeadingTitleHeadingHTML( "

合肥全屋定制口碑怎么判断 看这里

正文第一段。

", "合肥全屋定制口碑怎么判断 看这里", ) if strings.Contains(got, "

") || strings.Contains(got, "合肥全屋定制口碑怎么判断 看这里") { t.Fatalf("duplicate title heading was not removed: %q", got) } if !strings.Contains(got, "正文第一段") { t.Fatalf("body content was lost: %q", got) } } func TestEnterpriseSiteStripLeadingTitleHeadingHTMLKeepsDifferentHeading(t *testing.T) { input := "

行业现状

正文第一段。

" got := enterpriseSiteStripLeadingTitleHeadingHTML(input, "合肥全屋定制口碑怎么判断 看这里") if got != input { t.Fatalf("different leading heading changed: %q", got) } } func TestEnterpriseSiteStripLeadingTitleHeadingMarkdownRemovesDuplicateTitle(t *testing.T) { got := enterpriseSiteStripLeadingTitleHeadingMarkdown( "# 合肥全屋定制口碑怎么判断 看这里\n\n正文第一段。", "合肥全屋定制口碑怎么判断 看这里", ) if strings.Contains(got, "合肥全屋定制口碑怎么判断 看这里") { t.Fatalf("duplicate markdown title was not removed: %q", got) } if got != "正文第一段。" { t.Fatalf("markdown body = %q, want body only", got) } } func TestEnterpriseSiteStripLeadingTitleHeadingMarkdownRemovesPlainTitle(t *testing.T) { got := enterpriseSiteStripLeadingTitleHeadingMarkdown( "装修预算不够?2026安徽6款高性价比家居整理\n\n近年安徽本地全屋定制需求持续上涨。", "装修预算不够?2026安徽6款高性价比家居整理", ) if strings.Contains(got, "装修预算不够?2026安徽6款高性价比家居整理") { t.Fatalf("plain duplicate title was not removed: %q", got) } if !strings.HasPrefix(got, "近年安徽本地全屋定制需求持续上涨") { t.Fatalf("markdown body = %q, want body only", got) } } func TestEnterpriseSiteDescriptionAfterTitleStripSkipsDuplicateTitle(t *testing.T) { title := "合肥全屋定制口碑怎么判断 看这里" html := enterpriseSiteStripLeadingTitleHeadingHTML("

"+title+"

正文第一段。

", title) markdown := enterpriseSiteStripLeadingTitleHeadingMarkdown("# "+title+"\n\n正文第一段。", title) got := enterpriseSiteDescription(html, markdown) if strings.Contains(got, title) { t.Fatalf("description still contains duplicate title: %q", got) } if !strings.Contains(got, "正文第一段") { t.Fatalf("description = %q, want body text", got) } } func TestEnterpriseSiteDescriptionFallsBackToCleanMarkdown(t *testing.T) { got := enterpriseSiteDescription("", "## 2026年合肥全屋定制行业发展现状\n- 消费者需求升级。") if strings.Contains(got, "##") || strings.Contains(got, "- ") { t.Fatalf("description was not cleaned: %q", got) } if !strings.Contains(got, "消费者需求升级") { t.Fatalf("description = %q, want markdown text", got) } } func TestEnterpriseSiteNormalizeStoredContentRestoresDiffPatch(t *testing.T) { source := "# 2026安徽全屋定制行业\n\n## 引言\n正文第一段。" dmp := diffmatchpatch.New() patch := dmp.PatchToText(dmp.PatchMake("", source)) got := enterpriseSiteNormalizeStoredContent(patch) if got != source { t.Fatalf("restored content = %q, want %q", got, source) } if strings.Contains(got, "@@ -") { t.Fatalf("diff patch leaked into normalized content: %q", got) } } func TestEnterpriseSiteNormalizeStoredContentRepairsMalformedEditorImage(t *testing.T) { got := enterpriseSiteNormalizeStoredContent( `

`, ) if !strings.Contains(got, `align="center">2026安徽全屋定制行业") || !strings.Contains(got, "

正文第一段。

") { t.Fatalf("rendered HTML = %q, want UEditor-ready HTML", got) } } func TestEnterpriseSiteAbsoluteImageURLsRewritesRelativeAssetPaths(t *testing.T) { got := enterpriseSiteAbsoluteImageURLs( `

`, "http://127.0.0.1:8080", ) if strings.Count(got, "http://127.0.0.1:8080/api/public/assets/token") != 2 { t.Fatalf("image urls were not absolutized: %s", got) } } func TestPrepareEnterpriseSiteImageURLsEmbedsPublicAssetImages(t *testing.T) { objectKey := "tenants/7/images/body.png" token := publicasset.SignObjectKey(objectKey, "test-secret") storage := &enterpriseSiteImageObjectStorage{content: testEnterpriseSitePNG()} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "test-secret"}, })).WithObjectStorage(storage) got := svc.prepareEnterpriseSiteImageURLs( context.Background(), 7, 11, `

`, "http://127.0.0.1:8080", ) if storage.gotKey != objectKey { t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey) } if strings.Count(got, `data:image/png;base64,`) != 2 { t.Fatalf("image was not embedded in both image attrs: %s", got) } if strings.Contains(got, "/api/public/assets/") { t.Fatalf("public asset url leaked into enterprise site payload: %s", got) } } func TestPrepareEnterpriseSiteImageURLsEmbedsPublicAssetImagesWithStaleSignature(t *testing.T) { objectKey := "tenants/7/images/body.png" token := publicasset.SignObjectKey(objectKey, "old-secret") storage := &enterpriseSiteImageObjectStorage{content: testEnterpriseSitePNG()} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "new-secret"}, })).WithObjectStorage(storage) got := svc.prepareEnterpriseSiteImageURLs( context.Background(), 7, 11, `

`, "http://127.0.0.1:8080", ) if storage.gotKey != objectKey { t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey) } if !strings.Contains(got, `src="data:image/png;base64,`) { t.Fatalf("stale signed image was not embedded: %s", got) } if strings.Contains(got, "localhost:5178") || strings.Contains(got, "/api/public/assets/") { t.Fatalf("saas asset url leaked into enterprise site payload: %s", got) } } func TestPrepareEnterpriseSiteImageURLsStripsSaaSImageAttrs(t *testing.T) { objectKey := "tenants/7/images/body.png" token := publicasset.SignObjectKey(objectKey, "test-secret") storage := &enterpriseSiteImageObjectStorage{content: testEnterpriseSitePNG()} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "test-secret"}, })).WithObjectStorage(storage) got := svc.prepareEnterpriseSiteImageURLs( context.Background(), 7, 11, `

`, "http://127.0.0.1:8080", ) if !strings.Contains(got, `src="data:image/png;base64,`) { t.Fatalf("image was not embedded: %s", got) } if strings.Contains(got, "data-asset-id") || strings.Contains(got, "asset-id") { t.Fatalf("saas image attrs leaked into enterprise site payload: %s", got) } } func TestPrepareEnterpriseSiteImageURLsDropsUnavailableSaaSAssetImage(t *testing.T) { objectKey := "tenants/7/images/deleted.webp" token := publicasset.SignObjectKey(objectKey, "old-secret") storage := &enterpriseSiteImageObjectStorage{} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "new-secret"}, })).WithObjectStorage(storage) got := svc.prepareEnterpriseSiteImageURLs( context.Background(), 7, 11, `

前文

后文

`, "http://127.0.0.1:8080", ) if strings.Contains(got, "localhost:5178") || strings.Contains(got, "/api/public/assets/") { t.Fatalf("unavailable saas asset url leaked into enterprise site payload: %s", got) } if strings.Contains(got, "

`, "http://127.0.0.1:8080", ) if storage.gotKey != objectKey { t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey) } if !strings.Contains(got, `src="data:image/png;base64,`) { t.Fatalf("image was not embedded for incomplete asset path: %s", got) } if strings.Contains(got, "127.0.0.1") || strings.Contains(got, "api/public/assets") { t.Fatalf("incomplete asset path leaked into enterprise site payload: %s", got) } } func TestMarkdownRenderedEnterpriseSiteImageEmbedsIncompletePublicAssetPath(t *testing.T) { objectKey := "tenants/7/articles/42/images/body.webp" token := publicasset.SignObjectKey(objectKey, "test-secret") storage := &enterpriseSiteImageObjectStorage{content: testEnterpriseSitePNG()} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "test-secret"}, })).WithObjectStorage(storage) html, err := markdownToEnterpriseSiteHTML("正文\n\n![body](api/public/assets/" + token + "?format=png)") if err != nil { t.Fatalf("render markdown: %v", err) } got := svc.prepareEnterpriseSiteImageURLs(context.Background(), 7, 11, html, "http://127.0.0.1:8080") if storage.gotKey != objectKey { t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey) } if !strings.Contains(got, `src="data:image/png;base64,`) { t.Fatalf("markdown image was not embedded for incomplete asset path: %s", got) } if strings.Contains(got, "127.0.0.1") || strings.Contains(got, "api/public/assets") { t.Fatalf("markdown image asset path leaked into enterprise site payload: %s", got) } } func TestPrepareEnterpriseSitePublishAssetsEmbedsIncompleteCoverAssetPath(t *testing.T) { objectKey := "tenants/7/articles/42/images/cover.webp" token := publicasset.SignObjectKey(objectKey, "test-secret") storage := &enterpriseSiteImageObjectStorage{content: testEnterpriseSitePNG()} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "test-secret"}, Server: config.ServerConfig{PublicBaseURL: "http://127.0.0.1:8080"}, })).WithObjectStorage(storage) got := svc.prepareEnterpriseSiteCoverURL( context.Background(), 7, 11, "api/public/assets/"+token+"?format=png", nil, "http://127.0.0.1:8080", ) if storage.gotKey != objectKey { t.Fatalf("object key = %q, want %q", storage.gotKey, objectKey) } if !strings.HasPrefix(got, "data:image/png;base64,") { t.Fatalf("cover was not embedded as data URI: %q", got) } } func TestPrepareEnterpriseSiteCoverURLDropsUnavailableSaaSAssetURL(t *testing.T) { objectKey := "tenants/7/images/deleted.webp" token := publicasset.SignObjectKey(objectKey, "old-secret") storage := &enterpriseSiteImageObjectStorage{} svc := NewEnterpriseSiteService(nil, config.NewStaticProvider(&config.Config{ JWT: config.JWTConfig{Secret: "new-secret"}, Server: config.ServerConfig{PublicBaseURL: "http://127.0.0.1:8080"}, })).WithObjectStorage(storage) got := svc.prepareEnterpriseSiteCoverURL( context.Background(), 7, 11, "http://localhost:5178/api/public/assets/"+token, nil, "http://127.0.0.1:8080", ) if got != "" { t.Fatalf("unavailable saas cover should not be sent to pbootcms, got %q", got) } } func TestEnterpriseSiteNormalizeLegacyPublishErrorState(t *testing.T) { errorMessage := "图片下载失败" item := EnterpriseSiteConnectionResponse{ Status: "error", LastError: &errorMessage, LatestRecord: &EnterpriseSiteLatestPublishRecord{ Status: "failed", ErrorMessage: &errorMessage, }, } enterpriseSiteNormalizeLegacyPublishErrorState(&item) if item.Status != "connected" { t.Fatalf("status = %q, want connected", item.Status) } if item.LastError != nil { t.Fatalf("last error = %q, want nil", *item.LastError) } } func TestEnterpriseSiteNormalizeLegacyPublishErrorStateKeepsPingError(t *testing.T) { lastError := "CMS request failed" recordError := "图片下载失败" item := EnterpriseSiteConnectionResponse{ Status: "error", LastError: &lastError, LatestRecord: &EnterpriseSiteLatestPublishRecord{ Status: "failed", ErrorMessage: &recordError, }, } enterpriseSiteNormalizeLegacyPublishErrorState(&item) if item.Status != "error" { t.Fatalf("status = %q, want error", item.Status) } if item.LastError == nil || *item.LastError != lastError { t.Fatalf("last error changed: %#v", item.LastError) } } func TestSanitizeEnterpriseSiteErrorTranslatesWordPressAuthFailure(t *testing.T) { got := sanitizeEnterpriseSiteError(&wordpressRequestError{ message: "wordpress wp/v2/users/me returned http 401: rest_not_logged_in: 您目前没有登录。", status: 401, code: "rest_not_logged_in", }) want := "WordPress 认证失败,请使用 WordPress 用户名和 Application Password" if got != want { t.Fatalf("sanitize error = %q, want %q", got, want) } } type enterpriseSiteImageObjectStorage struct { content []byte gotKey string } func (s *enterpriseSiteImageObjectStorage) Validate() error { return nil } func (s *enterpriseSiteImageObjectStorage) PutBytes(context.Context, string, []byte, string) error { return nil } func (s *enterpriseSiteImageObjectStorage) GetBytes(_ context.Context, objectKey string) ([]byte, error) { s.gotKey = objectKey return s.content, nil } func (s *enterpriseSiteImageObjectStorage) Exists(context.Context, string) (bool, error) { return true, nil } func (s *enterpriseSiteImageObjectStorage) Delete(context.Context, string) error { return nil } func (s *enterpriseSiteImageObjectStorage) PublicURL(string) (string, error) { return "", nil } func (s *enterpriseSiteImageObjectStorage) DownloadURL(string) (string, error) { return "", nil } func testEnterpriseSitePNG() []byte { return []byte{ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, } }