fix(wiki): purge through the plugin page actions and count reset deletions
ci / ci (pull_request) Successful in 3m52s
ci / ci (pull_request) Successful in 3m52s
deploy-wiki purged pages with DELETE /api/v3/topics/{tid}. A NodeBB
running nodebb-plugin-westgate-wiki refuses that for topics in wiki
categories, because wiki revision history is plugin-owned, so every
purge failed with HTTP 400 and the deploy exited 1. Purge now goes
through the plugin's own page actions: tombstone the page, then
hard-purge it, since a page must be tombstoned before it can be purged.
Archiving is unaffected: it rewrites the page through the ordinary post
edit the plugin allows, rather than deleting anything.
Every fake NodeBB in the deploy tests now goes through one constructor
that refuses native topic mutation the way the plugin does. The old
fakes answered the core API, which is why this shipped green.
A namespace reset can reach pages NodeBB will not delete at all - the
wiki home topic above all - so those are skipped rather than aborting
the reset. NodeBB answers 403 for that and for a token without purge
privileges alike, and the response cannot tell them apart; what can is
scope, so a category where nothing at all could be deleted still fails
the run.
Under --reset-managed-namespaces the preview reported stale: 0 on a run
that would delete every topic in the managed categories, because the
reset purge never went through stale computation. The reset deletions
are now counted as stale, which is the number callers word their
destructive-policy warning around, and the summary names how many of
them the manifest has no record of writing - the deletions a re-seed
cannot undo.
Closes #99
Closes #100
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -35,7 +36,7 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
updateCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer nodebb-token" {
|
||||
t.Fatalf("unexpected authorization header %q", got)
|
||||
}
|
||||
@@ -50,7 +51,7 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -92,7 +93,7 @@ func TestDeployWikiReportsPlanningProgressBeforeRemoteWork(t *testing.T) {
|
||||
}
|
||||
|
||||
progress := []string{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -100,7 +101,7 @@ func TestDeployWikiReportsPlanningProgressBeforeRemoteWork(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -174,7 +175,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
||||
}
|
||||
|
||||
progress := []string{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -194,7 +195,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -228,7 +229,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
||||
|
||||
func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
@@ -240,7 +241,7 @@ func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *te
|
||||
"nextCursor": "same-cursor",
|
||||
},
|
||||
})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newNodeBBClient(server.URL, "nodebb-token")
|
||||
@@ -258,7 +259,7 @@ func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *te
|
||||
|
||||
func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T) {
|
||||
requests := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pages := []map[string]any{
|
||||
@@ -276,7 +277,7 @@ func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T
|
||||
"nextCursor": "same-cursor",
|
||||
},
|
||||
})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
client := newNodeBBClient(server.URL, "nodebb-token")
|
||||
@@ -480,7 +481,7 @@ func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -508,7 +509,7 @@ func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -731,7 +732,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
createCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer nodebb-token" {
|
||||
t.Fatalf("unexpected authorization header %q", got)
|
||||
}
|
||||
@@ -769,7 +770,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
@@ -848,7 +849,7 @@ func TestDeployWikiRepairsManifestedPageMissingSourceContentSync(t *testing.T) {
|
||||
}
|
||||
|
||||
updateCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -876,7 +877,7 @@ func TestDeployWikiRepairsManifestedPageMissingSourceContentSync(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -951,7 +952,7 @@ func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitl
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/5/pages" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||
@@ -983,7 +984,7 @@ func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitl
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1010,7 +1011,7 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMi
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/5/pages" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||
@@ -1038,7 +1039,7 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMi
|
||||
},
|
||||
},
|
||||
})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1080,7 +1081,7 @@ func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1109,7 +1110,7 @@ func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1166,7 +1167,7 @@ func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1195,7 +1196,7 @@ func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1258,9 +1259,9 @@ func TestDeployWikiDoesNotRenameHeadinglessPageToPageIDFallback(t *testing.T) {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("headingless unchanged page must not call NodeBB, got %s %s", r.Method, r.URL.String())
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1320,7 +1321,7 @@ func TestDeployWikiRenamesBrokenHeadinglessFallbackTitleBackToPageIndexTitle(t *
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1345,7 +1346,7 @@ func TestDeployWikiRenamesBrokenHeadinglessFallbackTitleBackToPageIndexTitle(t *
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1396,7 +1397,7 @@ func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t
|
||||
|
||||
createCalls := 0
|
||||
var updated string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/9/pages":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1442,7 +1443,7 @@ func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
@@ -1509,7 +1510,7 @@ func TestDeployWikiMergesHTMLManagedAndManualRegions(t *testing.T) {
|
||||
}
|
||||
|
||||
var updated string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1533,7 +1534,7 @@ func TestDeployWikiMergesHTMLManagedAndManualRegions(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1589,7 +1590,7 @@ func TestDeployWikiUpdateAcquiresWestgateWikiEditLock(t *testing.T) {
|
||||
|
||||
lockAcquired := false
|
||||
var updateToken string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1621,7 +1622,7 @@ func TestDeployWikiUpdateAcquiresWestgateWikiEditLock(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1668,7 +1669,7 @@ func TestDeployWikiCreateCollisionAdoptsExistingNodeBBPage(t *testing.T) {
|
||||
|
||||
createCalls := 0
|
||||
var updated string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/9/pages" && r.URL.Query().Get("q") == "":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1719,7 +1720,7 @@ func TestDeployWikiCreateCollisionAdoptsExistingNodeBBPage(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
@@ -1771,7 +1772,7 @@ func TestDeployWikiCreateCollisionSearchesCanonicalTitleSegment(t *testing.T) {
|
||||
|
||||
createCalls := 0
|
||||
var updated string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/58/pages" && r.URL.Query().Get("q") == "":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -1828,7 +1829,7 @@ func TestDeployWikiCreateCollisionSearchesCanonicalTitleSegment(t *testing.T) {
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
@@ -1868,7 +1869,7 @@ func TestDeployWikiReportsAndArchivesStalePages(t *testing.T) {
|
||||
}
|
||||
|
||||
var archived string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/edit-lock" {
|
||||
respondWikiEditLock(t, w, r, 7, "archive-lock")
|
||||
return
|
||||
@@ -1889,7 +1890,7 @@ func TestDeployWikiReportsAndArchivesStalePages(t *testing.T) {
|
||||
archived = req.Content
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pid": 42, "tid": 7}})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
report, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1945,9 +1946,9 @@ func TestDeployWikiDryRunPlansTrackedStalePagePurge(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run purge must not call NodeBB, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1984,9 +1985,9 @@ func TestDeployWikiPurgeRefusesStaleManifestEntryWithoutTopicID(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("missing-topic-id purge must not call NodeBB, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2017,15 +2018,12 @@ func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
purgeCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete || r.URL.Path != "/api/v3/topics/7" {
|
||||
var calls []string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if !answerWikiPagePurge(t, w, r, &calls) {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
purgeCalls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2041,8 +2039,8 @@ func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
||||
if result.Stale != 1 || result.Purged != 1 {
|
||||
t.Fatalf("expected one stale purge, got %#v", result)
|
||||
}
|
||||
if purgeCalls != 1 {
|
||||
t.Fatalf("expected one NodeBB topic purge call, got %d", purgeCalls)
|
||||
if strings.Join(calls, ",") != "tombstone:7,hard-purge:7" {
|
||||
t.Fatalf("expected the wiki plugin page actions to purge topic 7, got %#v", calls)
|
||||
}
|
||||
if _, ok := loadDeployManifest(manifestPath).Pages["skills:retired"]; ok {
|
||||
t.Fatalf("expected purged stale manifest entry to be removed")
|
||||
@@ -2075,18 +2073,17 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
||||
}
|
||||
|
||||
var calls []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if answerWikiPagePurge(t, w, r, &calls) {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/api/v3/topics/7":
|
||||
calls = append(calls, "purge")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/v3/topics":
|
||||
calls = append(calls, "create")
|
||||
if len(calls) != 2 || calls[0] != "purge" {
|
||||
if strings.Join(calls, ",") != "tombstone:7,hard-purge:7,create" {
|
||||
t.Fatalf("expected stale purge before create, got calls %#v", calls)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -2102,7 +2099,7 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2120,7 +2117,7 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
||||
if result.Created != 1 || result.Purged != 1 {
|
||||
t.Fatalf("expected one create and one purge, got %#v", result)
|
||||
}
|
||||
if strings.Join(calls, ",") != "purge,create" {
|
||||
if strings.Join(calls, ",") != "tombstone:7,hard-purge:7,create" {
|
||||
t.Fatalf("expected purge before create, got %#v", calls)
|
||||
}
|
||||
}
|
||||
@@ -2150,8 +2147,12 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
const resetCalls = "tombstone:7,hard-purge:7,tombstone:8,hard-purge:8"
|
||||
var calls []string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if answerWikiPagePurge(t, w, r, &calls) {
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -2164,17 +2165,9 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
"hasMore": false,
|
||||
},
|
||||
})
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/api/v3/topics/7":
|
||||
calls = append(calls, "purge:7")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/api/v3/topics/8":
|
||||
calls = append(calls, "purge:8")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/v3/topics":
|
||||
calls = append(calls, "create")
|
||||
if strings.Join(calls, ",") != "purge:7,purge:8,create" {
|
||||
if strings.Join(calls, ",") != resetCalls+",create" {
|
||||
t.Fatalf("expected namespace reset purges before create, got %#v", calls)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -2190,7 +2183,7 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2205,10 +2198,21 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions reset-managed-namespaces failed: %v", err)
|
||||
}
|
||||
if result.Created != 1 || result.Purged != 2 || result.Stale != 0 {
|
||||
if result.Created != 1 || result.Purged != 2 {
|
||||
t.Fatalf("expected one create and two namespace purges, got %#v", result)
|
||||
}
|
||||
if strings.Join(calls, ",") != "purge:7,purge:8,create" {
|
||||
// Callers word their destructive-policy warning around the stale count, so
|
||||
// reset deletions have to show up there rather than on a separate path the
|
||||
// preview never mentions.
|
||||
if result.Stale != 2 || result.ResetPurged != 2 {
|
||||
t.Fatalf("expected reset deletions counted as stale, got %#v", result)
|
||||
}
|
||||
// tid 7 is the manifest's own page; tid 8 is a topic the deployer never
|
||||
// wrote, and re-seeding cannot bring it back.
|
||||
if result.ResetUnrecognized != 1 {
|
||||
t.Fatalf("expected one unrecognized reset deletion, got %#v", result)
|
||||
}
|
||||
if strings.Join(calls, ",") != resetCalls+",create" {
|
||||
t.Fatalf("expected reset purges before create, got %#v", calls)
|
||||
}
|
||||
manifest := loadDeployManifest(manifestPath)
|
||||
@@ -2236,9 +2240,9 @@ func TestDeployWikiResetManagedNamespacesRequiresCreateForLocalPages(t *testing.
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("reset without --create must fail before remote calls, got %s %s", r.Method, r.URL.String())
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2270,21 +2274,21 @@ func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T)
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
seen := map[string]int{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
call, ok := wikiPagePurgeCall(t, r)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
seen[r.URL.Path]++
|
||||
seen[call]++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch r.URL.Path {
|
||||
case "/api/v3/topics/7":
|
||||
// Topic 7 is already gone, so the plugin cannot find the page to
|
||||
// tombstone; topic 8 still exists and purges normally.
|
||||
if call == "tombstone:7" {
|
||||
http.Error(w, `{"status":{"message":"topic not found"}}`, http.StatusNotFound)
|
||||
case "/api/v3/topics/8":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
default:
|
||||
t.Fatalf("unexpected purge path %s", r.URL.Path)
|
||||
return
|
||||
}
|
||||
}))
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2300,8 +2304,11 @@ func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T)
|
||||
if result.Stale != 2 || result.Purged != 2 {
|
||||
t.Fatalf("expected two stale purges, got %#v", result)
|
||||
}
|
||||
if seen["/api/v3/topics/7"] != 1 || seen["/api/v3/topics/8"] != 1 {
|
||||
t.Fatalf("expected one purge call for each tracked topic, got %#v", seen)
|
||||
if seen["tombstone:7"] != 1 || seen["hard-purge:7"] != 0 {
|
||||
t.Fatalf("expected the already-missing topic to stop after tombstone, got %#v", seen)
|
||||
}
|
||||
if seen["tombstone:8"] != 1 || seen["hard-purge:8"] != 1 {
|
||||
t.Fatalf("expected the surviving topic to be tombstoned then purged, got %#v", seen)
|
||||
}
|
||||
manifest := loadDeployManifest(manifestPath)
|
||||
if _, ok := manifest.Pages["skills:retired"]; ok {
|
||||
@@ -2339,9 +2346,9 @@ func TestDeployWikiPurgeDoesNotTargetCurrentGeneratedPages(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("current generated page must not call NodeBB during matching-hash deploy, got %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2385,3 +2392,187 @@ func respondWikiEditLock(t *testing.T, w http.ResponseWriter, r *http.Request, e
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"status": "ok", "tid": expectedTID, "token": token}})
|
||||
}
|
||||
|
||||
// newFakeNodeBB stands up a fake NodeBB that behaves like one running
|
||||
// nodebb-plugin-westgate-wiki: native topic mutation on wiki categories is
|
||||
// refused, because wiki revision history is plugin-owned. Every deploy test
|
||||
// goes through here, so a deployer that reaches for the core topic API fails in
|
||||
// CI the way it fails in production rather than passing against a fake that is
|
||||
// more permissive than the real thing.
|
||||
func newFakeNodeBB(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/v3/topics/") && (r.Method == http.MethodDelete || r.Method == http.MethodPut) {
|
||||
http.Error(
|
||||
w,
|
||||
`{"status":{"code":"bad-request","message":"Use the wiki page actions to delete, restore, or purge wiki pages."}}`,
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
handler(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
// wikiPagePurgeCall recognizes the plugin page actions a purge goes through and
|
||||
// reports them as "<action>:<tid>" so tests can assert the order.
|
||||
func wikiPagePurgeCall(t *testing.T, r *http.Request) (string, bool) {
|
||||
t.Helper()
|
||||
var action string
|
||||
switch {
|
||||
case r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/page/tombstone":
|
||||
action = "tombstone"
|
||||
case r.Method == http.MethodDelete && r.URL.Path == "/api/v3/plugins/westgate-wiki/page/hard-purge":
|
||||
action = "hard-purge"
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
var req struct {
|
||||
TID int `json:"tid"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatalf("decode wiki %s request: %v", action, err)
|
||||
}
|
||||
if req.TID == 0 {
|
||||
t.Fatalf("expected wiki %s request to carry a topic id", action)
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", action, req.TID), true
|
||||
}
|
||||
|
||||
// answerWikiPagePurge records and acknowledges a plugin purge action, which is
|
||||
// what most fakes want to do with one.
|
||||
func answerWikiPagePurge(t *testing.T, w http.ResponseWriter, r *http.Request, calls *[]string) bool {
|
||||
t.Helper()
|
||||
call, ok := wikiPagePurgeCall(t, r)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
*calls = append(*calls, call)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
return true
|
||||
}
|
||||
|
||||
// resetPurgeFixture builds a source tree, a deploy manifest, and the reset
|
||||
// options the namespace-reset tests share.
|
||||
func resetPurgeFixture(t *testing.T, namespaces map[string]int, tidsByCID map[int][]int) (DeployWikiOptions, func(w http.ResponseWriter, r *http.Request) bool) {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "skills"), 0755); err != nil {
|
||||
t.Fatalf("create source dir: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
listPages := func(w http.ResponseWriter, r *http.Request) bool {
|
||||
if r.Method != http.MethodGet {
|
||||
return false
|
||||
}
|
||||
var cid int
|
||||
if _, err := fmt.Sscanf(r.URL.Path, "/api/v3/plugins/westgate-wiki/namespace/%d/pages", &cid); err != nil {
|
||||
return false
|
||||
}
|
||||
tids, ok := tidsByCID[cid]
|
||||
if !ok {
|
||||
t.Fatalf("unexpected namespace listing for category %d", cid)
|
||||
}
|
||||
pages := make([]map[string]any, 0, len(tids))
|
||||
for _, tid := range tids {
|
||||
pages = append(pages, map[string]any{"tid": tid, "title": fmt.Sprintf("Page %d", tid)})
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": pages, "hasMore": false}})
|
||||
return true
|
||||
}
|
||||
|
||||
return DeployWikiOptions{
|
||||
SourceDir: sourceDir,
|
||||
ManifestPath: manifestPath,
|
||||
Token: "nodebb-token",
|
||||
CategoryIDs: namespaces,
|
||||
AllowCreates: true,
|
||||
ResetManagedNamespaces: true,
|
||||
}, listPages
|
||||
}
|
||||
|
||||
func TestDeployWikiResetSkipsTopicsNodeBBRefusesToDelete(t *testing.T) {
|
||||
opts, listPages := resetPurgeFixture(t, map[string]int{"skills": 3}, map[int][]int{3: {5, 6}})
|
||||
var calls []string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if listPages(w, r) {
|
||||
return
|
||||
}
|
||||
call, ok := wikiPagePurgeCall(t, r)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
calls = append(calls, call)
|
||||
// Topic 5 stands in for the wiki home page, which the plugin excludes
|
||||
// from tombstone, restore, and purge alike.
|
||||
if call == "tombstone:5" {
|
||||
http.Error(w, `{"status":{"message":"[[error:no-privileges]]"}}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
})
|
||||
defer server.Close()
|
||||
opts.Endpoint = server.URL
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, opts, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions reset with an undeletable topic failed: %v", err)
|
||||
}
|
||||
if result.ResetSkipped != 1 || result.ResetPurged != 1 {
|
||||
t.Fatalf("expected one skipped and one completed reset deletion, got %#v", result)
|
||||
}
|
||||
if result.Purged != 1 || result.Stale != 1 {
|
||||
t.Fatalf("expected the skipped topic to be dropped from the counts, got %#v", result)
|
||||
}
|
||||
if result.ResetUnrecognized != 1 {
|
||||
t.Fatalf("expected only the purged unrecognized topic to be counted, got %#v", result)
|
||||
}
|
||||
if strings.Join(calls, ",") != "tombstone:5,tombstone:6,hard-purge:6" {
|
||||
t.Fatalf("expected the reset to continue past the refused topic, got %#v", calls)
|
||||
}
|
||||
}
|
||||
|
||||
// NodeBB answers 403 both for the wiki home page and for a token without purge
|
||||
// privileges. A category where nothing could be deleted at all is the second
|
||||
// case, and reporting it as a completed reset would leave the manifest claiming
|
||||
// a fresh start over pages that are all still there.
|
||||
func TestDeployWikiResetFailsWhenACategoryRefusesEveryDeletion(t *testing.T) {
|
||||
opts, listPages := resetPurgeFixture(t,
|
||||
map[string]int{"skills": 3, "feats": 4},
|
||||
map[int][]int{3: {5, 6}, 4: {7, 8}},
|
||||
)
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if listPages(w, r) {
|
||||
return
|
||||
}
|
||||
call, ok := wikiPagePurgeCall(t, r)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
// Category 4 holds topics 7 and 8, and the token can purge neither.
|
||||
if strings.HasSuffix(call, ":7") || strings.HasSuffix(call, ":8") {
|
||||
http.Error(w, `{"status":{"message":"[[error:no-privileges]]"}}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||
})
|
||||
defer server.Close()
|
||||
opts.Endpoint = server.URL
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, opts, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "wiki purge privileges") {
|
||||
t.Fatalf("expected a category that deleted nothing to fail loudly, got %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "4") {
|
||||
t.Fatalf("expected the failure to name the refusing category, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user