feat(enterprise-site): add WordPress support and scheduled auto-publish to sites
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>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user