c7bad83496
Add WordPress as an enterprise-site CMS type alongside pbootcms, and let schedule tasks auto-publish generated articles to enterprise sites. - WordPress connection/publisher service and tests; register wordpress media platform and relax cms_type CHECK constraint - New publish_enterprise_site_targets JSONB column on schedule_tasks with array type constraint; thread targets through scheduler dispatch, prompt/KOL generation, and worker - Admin UI: enterprise-site targets in generate/schedule/publish flows, KOL package form, MediaView, and i18n strings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166 lines
5.6 KiB
Go
166 lines
5.6 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestWordPressPublisherCategoriesUsesRESTAPIAndBasicAuth(t *testing.T) {
|
|
const username = "editor@example.com"
|
|
const applicationPassword = "abcd efgh ijkl mnop"
|
|
var gotAuth string
|
|
var gotPath string
|
|
var gotQuery string
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotAuth = r.Header.Get("Authorization")
|
|
gotPath = r.URL.Path
|
|
gotQuery = r.URL.RawQuery
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Header().Set("X-WP-TotalPages", "1")
|
|
_, _ = w.Write([]byte(`[
|
|
{"id": 12, "name": "案例", "slug": "case", "parent": 0, "count": 3, "link": "https://example.test/category/case/"}
|
|
]`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
publisher := newWordPressPublisher(server.Client())
|
|
items, raw, err := publisher.Categories(context.Background(), enterpriseSiteCredential{
|
|
SiteURL: server.URL,
|
|
AppID: username,
|
|
Secret: applicationPassword,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sync wordpress categories: %v", err)
|
|
}
|
|
|
|
wantAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+applicationPassword))
|
|
if gotAuth != wantAuth {
|
|
t.Fatalf("authorization = %q, want %q", gotAuth, wantAuth)
|
|
}
|
|
if gotPath != "/index.php" {
|
|
t.Fatalf("path = %q, want wordpress categories endpoint", gotPath)
|
|
}
|
|
if !strings.Contains(gotQuery, "rest_route=%2Fwp%2Fv2%2Fcategories") ||
|
|
!strings.Contains(gotQuery, "per_page=100") ||
|
|
!strings.Contains(gotQuery, "hide_empty=false") {
|
|
t.Fatalf("query = %q, want wordpress rest_route, per_page and hide_empty", gotQuery)
|
|
}
|
|
if len(items) != 1 || items[0].RemoteID != "12" || items[0].Name != "案例" {
|
|
t.Fatalf("items = %#v, want parsed wordpress category", items)
|
|
}
|
|
if len(raw) != 1 || raw[0]["slug"] != "case" {
|
|
t.Fatalf("raw = %#v, want preserved category payload", raw)
|
|
}
|
|
}
|
|
|
|
func TestWordPressPublisherPingReturnsSafeRawSummary(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case strings.Contains(r.URL.RawQuery, "rest_route=%2Fwp%2Fv2%2Fusers%2Fme"):
|
|
_, _ = w.Write([]byte(`{"id": 7, "name": "admin", "email": "admin@example.test", "capabilities": {"administrator": true}}`))
|
|
default:
|
|
_, _ = w.Write([]byte(`{"name": "Example", "home": "https://example.test", "routes": {"/wp/v2/users/me": {"sensitive": true}}}`))
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
publisher := newWordPressPublisher(server.Client())
|
|
capability, raw, err := publisher.Ping(context.Background(), enterpriseSiteCredential{
|
|
SiteURL: server.URL,
|
|
AppID: "editor",
|
|
Secret: "application-password",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ping wordpress: %v", err)
|
|
}
|
|
if capability.SiteName == nil || *capability.SiteName != "Example" || !capability.APIAuth {
|
|
t.Fatalf("capability = %#v, want sanitized wordpress capability", capability)
|
|
}
|
|
if raw["api_auth"] != true || raw["authenticated"] != true {
|
|
t.Fatalf("raw = %#v, want safe auth summary", raw)
|
|
}
|
|
if _, ok := raw["routes"]; ok {
|
|
t.Fatalf("raw leaked wordpress index routes: %#v", raw)
|
|
}
|
|
if user, ok := raw["authenticated_user"].(map[string]any); ok || user != nil {
|
|
t.Fatalf("raw leaked authenticated user body: %#v", raw)
|
|
}
|
|
if strings.Contains(fmt.Sprintf("%v", raw), "admin@example.test") {
|
|
t.Fatalf("raw leaked authenticated user email: %#v", raw)
|
|
}
|
|
}
|
|
|
|
func TestWordPressPublisherPublishCreatesPost(t *testing.T) {
|
|
var gotPath string
|
|
var gotQuery string
|
|
var gotPayload map[string]any
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
gotPath = r.URL.Path
|
|
gotQuery = r.URL.RawQuery
|
|
if r.Method != http.MethodPost {
|
|
t.Fatalf("method = %s, want POST", r.Method)
|
|
}
|
|
if _, _, ok := r.BasicAuth(); !ok {
|
|
t.Fatalf("missing basic auth")
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&gotPayload); err != nil {
|
|
t.Fatalf("decode request body: %v", err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id": 88, "link": "https://example.test/post/88/", "status": "publish"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
publisher := newWordPressPublisher(server.Client())
|
|
result, raw, err := publisher.Publish(context.Background(), enterpriseSiteCredential{
|
|
SiteURL: server.URL,
|
|
AppID: "editor",
|
|
Secret: "application-password",
|
|
}, cmsPublishRequest{
|
|
Title: "测试标题",
|
|
ContentHTML: "<p>正文</p>",
|
|
CategoryID: "12",
|
|
Description: "摘要",
|
|
PublishType: "draft",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("publish wordpress post: %v", err)
|
|
}
|
|
|
|
if gotPath != "/index.php" {
|
|
t.Fatalf("path = %q, want wordpress posts endpoint", gotPath)
|
|
}
|
|
if !strings.Contains(gotQuery, "rest_route=%2Fwp%2Fv2%2Fposts") {
|
|
t.Fatalf("query = %q, want wordpress posts rest_route", gotQuery)
|
|
}
|
|
if gotPayload == nil {
|
|
t.Fatalf("missing request payload")
|
|
}
|
|
if gotPayload["title"] != "测试标题" || gotPayload["content"] != "<p>正文</p>" || gotPayload["status"] != "draft" {
|
|
t.Fatalf("payload = %#v, want wordpress post body", gotPayload)
|
|
}
|
|
categories, ok := gotPayload["categories"].([]any)
|
|
if !ok || len(categories) != 1 {
|
|
t.Fatalf("categories = %#v, want [12]", gotPayload["categories"])
|
|
}
|
|
categoryID, ok := categories[0].(float64)
|
|
if !ok || categoryID != 12 {
|
|
t.Fatalf("category id = %#v, want 12", categories[0])
|
|
}
|
|
if result.RemoteID != "88" || result.URL != "https://example.test/post/88/" {
|
|
t.Fatalf("result = %#v, want wordpress post result", result)
|
|
}
|
|
if raw["id"] != int64(88) {
|
|
t.Fatalf("raw = %#v, want saved wordpress response", raw)
|
|
}
|
|
}
|