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:
+20
-11
@@ -927,19 +927,28 @@ func (c *topdataConsole) emitWikiBuildResult(outputDir string, pageCount int, st
|
|||||||
fmt.Fprintf(c.stdout, "wiki status: %s\n", status)
|
fmt.Fprintf(c.stdout, "wiki status: %s\n", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *topdataConsole) emitWikiDeployResult(localPages, created, updated, skipped, stale, archived, purged, drifted int, manifest string) {
|
func (c *topdataConsole) emitWikiDeployResult(result topdata.DeployResult) {
|
||||||
spin.linebreak()
|
spin.linebreak()
|
||||||
fmt.Fprintln(c.stdout, "Deploy Wiki ----------")
|
fmt.Fprintln(c.stdout, "Deploy Wiki ----------")
|
||||||
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
|
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
|
||||||
fmt.Fprintf(c.stdout, "local pages: %d\n", localPages)
|
fmt.Fprintf(c.stdout, "local pages: %d\n", result.LocalPages)
|
||||||
fmt.Fprintf(c.stdout, "created: %d\n", created)
|
fmt.Fprintf(c.stdout, "created: %d\n", result.Created)
|
||||||
fmt.Fprintf(c.stdout, "updated: %d\n", updated)
|
fmt.Fprintf(c.stdout, "updated: %d\n", result.Updated)
|
||||||
fmt.Fprintf(c.stdout, "skipped: %d\n", skipped)
|
fmt.Fprintf(c.stdout, "skipped: %d\n", result.Skipped)
|
||||||
fmt.Fprintf(c.stdout, "stale: %d\n", stale)
|
fmt.Fprintf(c.stdout, "stale: %d\n", result.Stale)
|
||||||
fmt.Fprintf(c.stdout, "archived: %d\n", archived)
|
fmt.Fprintf(c.stdout, "archived: %d\n", result.Archived)
|
||||||
fmt.Fprintf(c.stdout, "purged: %d\n", purged)
|
fmt.Fprintf(c.stdout, "purged: %d\n", result.Purged)
|
||||||
fmt.Fprintf(c.stdout, "drifted: %d\n", drifted)
|
fmt.Fprintf(c.stdout, "drifted: %d\n", result.Drifted)
|
||||||
fmt.Fprintf(c.stdout, "manifest: %s\n", c.relPath(manifest))
|
if result.ResetPurged > 0 || result.ResetSkipped > 0 {
|
||||||
|
fmt.Fprintf(c.stdout, "namespace reset: %d (unrecognized: %d)\n", result.ResetPurged, result.ResetUnrecognized)
|
||||||
|
}
|
||||||
|
if result.ResetUnrecognized > 0 {
|
||||||
|
fmt.Fprintln(c.stdout, " unrecognized pages were not written by this deployer; recreating them is not possible")
|
||||||
|
}
|
||||||
|
if result.ResetSkipped > 0 {
|
||||||
|
fmt.Fprintf(c.stdout, " reset skipped: %d (NodeBB refused deletion, e.g. the wiki home page)\n", result.ResetSkipped)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(c.stdout, "manifest: %s\n", c.relPath(result.Manifest))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *topdataConsole) relPath(path string) string {
|
func (c *topdataConsole) relPath(path string) string {
|
||||||
@@ -1579,7 +1588,7 @@ func runDeployWiki(ctx context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
console.emitWikiDeployResult(result.LocalPages, result.Created, result.Updated, result.Skipped, result.Stale, result.Archived, result.Purged, result.Drifted, result.Manifest)
|
console.emitWikiDeployResult(result)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline"
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline"
|
||||||
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/topdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseBuildHAKArgsContentAddressedRoot(t *testing.T) {
|
func TestParseBuildHAKArgsContentAddressedRoot(t *testing.T) {
|
||||||
@@ -218,7 +219,16 @@ func TestTopdataConsoleDebugProgressAndRelativePaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.progress("NodeBB wiki plan: create 1, update 2, skip 3, stale 4, archive 5, purge 6, drift 0")
|
console.progress("NodeBB wiki plan: create 1, update 2, skip 3, stale 4, archive 5, purge 6, drift 0")
|
||||||
console.emitWikiDeployResult(10, 1, 2, 3, 4, 5, 6, 0, "/workspace/project/build/wiki/deploy-manifest.json")
|
console.emitWikiDeployResult(topdata.DeployResult{
|
||||||
|
LocalPages: 10,
|
||||||
|
Created: 1,
|
||||||
|
Updated: 2,
|
||||||
|
Skipped: 3,
|
||||||
|
Stale: 4,
|
||||||
|
Archived: 5,
|
||||||
|
Purged: 6,
|
||||||
|
Manifest: "/workspace/project/build/wiki/deploy-manifest.json",
|
||||||
|
})
|
||||||
|
|
||||||
output := stdout.String()
|
output := stdout.String()
|
||||||
if !strings.Contains(output, "NodeBB wiki plan") {
|
if !strings.Contains(output, "NodeBB wiki plan") {
|
||||||
@@ -229,6 +239,53 @@ func TestTopdataConsoleDebugProgressAndRelativePaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTopdataConsoleReportsManagedNamespaceReset(t *testing.T) {
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
console := &topdataConsole{
|
||||||
|
stdout: &stdout,
|
||||||
|
projectRoot: "/workspace/project",
|
||||||
|
projectName: "Test Module",
|
||||||
|
commandName: "deploy-wiki",
|
||||||
|
commandLabel: "Deploy Wiki",
|
||||||
|
}
|
||||||
|
|
||||||
|
console.emitWikiDeployResult(topdata.DeployResult{
|
||||||
|
LocalPages: 1200,
|
||||||
|
Created: 1200,
|
||||||
|
Stale: 1213,
|
||||||
|
Purged: 1213,
|
||||||
|
ResetPurged: 1213,
|
||||||
|
ResetUnrecognized: 13,
|
||||||
|
ResetSkipped: 1,
|
||||||
|
Manifest: "/workspace/project/build/wiki/deploy-manifest.json",
|
||||||
|
})
|
||||||
|
|
||||||
|
// The counts are what an operator reads to decide whether a destructive run
|
||||||
|
// is safe, so each has to reach the output on a line that names what it
|
||||||
|
// counts; the wording around them is free to change.
|
||||||
|
output := stdout.String()
|
||||||
|
var staleLine, resetLine, skippedLine string
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(line, "stale"):
|
||||||
|
staleLine = line
|
||||||
|
case strings.Contains(line, "skipped"):
|
||||||
|
skippedLine = line
|
||||||
|
case strings.Contains(line, "reset"):
|
||||||
|
resetLine = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(staleLine, "1213") {
|
||||||
|
t.Fatalf("expected reset deletions counted as stale, got %q", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(resetLine, "1213") || !strings.Contains(resetLine, "13") {
|
||||||
|
t.Fatalf("expected the reset line to carry both the total and the unrecognized count, got %q", output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(skippedLine, "1") {
|
||||||
|
t.Fatalf("expected refused reset deletions to be reported, got %q", output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseDeployWikiHelpListsPurgeStalePolicy(t *testing.T) {
|
func TestParseDeployWikiHelpListsPurgeStalePolicy(t *testing.T) {
|
||||||
_, err := parseDeployWikiArgs("deploy-wiki", []string{"--help"})
|
_, err := parseDeployWikiArgs("deploy-wiki", []string{"--help"})
|
||||||
if err == nil || !strings.Contains(err.Error(), "--stale-policy <report|archive|purge>") {
|
if err == nil || !strings.Contains(err.Error(), "--stale-policy <report|archive|purge>") {
|
||||||
|
|||||||
@@ -289,7 +289,8 @@ var Registry = []Builder{
|
|||||||
"--dry-run report changes without writing",
|
"--dry-run report changes without writing",
|
||||||
"--create allow missing pages to be created",
|
"--create allow missing pages to be created",
|
||||||
"--force update unchanged pages",
|
"--force update unchanged pages",
|
||||||
"--reset-managed-namespaces reset managed namespace state",
|
"--reset-managed-namespaces delete every page in the managed categories, including",
|
||||||
|
" pages this deployer never wrote, then recreate from source",
|
||||||
},
|
},
|
||||||
Aliases: []CommandAlias{{Name: "deploy-wiki"}},
|
Aliases: []CommandAlias{{Name: "deploy-wiki"}},
|
||||||
},
|
},
|
||||||
|
|||||||
+127
-28
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -52,7 +53,7 @@ type DeployWikiOptions struct {
|
|||||||
TitlePrefixMinLength int
|
TitlePrefixMinLength int
|
||||||
}
|
}
|
||||||
|
|
||||||
type deployResult struct {
|
type DeployResult struct {
|
||||||
LocalPages int
|
LocalPages int
|
||||||
Created int
|
Created int
|
||||||
Updated int
|
Updated int
|
||||||
@@ -63,6 +64,19 @@ type deployResult struct {
|
|||||||
Drifted int
|
Drifted int
|
||||||
Renamed int
|
Renamed int
|
||||||
Manifest string
|
Manifest string
|
||||||
|
// ResetPurged counts the deletions queued by --reset-managed-namespaces.
|
||||||
|
// They are also included in Stale and Purged, because callers warn about
|
||||||
|
// destructive policies in terms of the stale count: sow-topdata's
|
||||||
|
// deploy-wiki wrapper prints "of the pages counted as 'stale' below, they
|
||||||
|
// will be DELETED from NodeBB" directly above this block.
|
||||||
|
ResetPurged int
|
||||||
|
// ResetUnrecognized counts the subset of ResetPurged that the deploy
|
||||||
|
// manifest has no record of writing. Those deletions are the ones a
|
||||||
|
// re-seed cannot undo.
|
||||||
|
ResetUnrecognized int
|
||||||
|
// ResetSkipped counts reset targets NodeBB refused to delete, such as the
|
||||||
|
// wiki home topic, which the plugin excludes from tombstone and purge.
|
||||||
|
ResetSkipped int
|
||||||
}
|
}
|
||||||
|
|
||||||
type wikiDeployPage struct {
|
type wikiDeployPage struct {
|
||||||
@@ -100,6 +114,12 @@ type wikiDeployPlan struct {
|
|||||||
Content string
|
Content string
|
||||||
RemoteHash string
|
RemoteHash string
|
||||||
Title string
|
Title string
|
||||||
|
// Reset marks a purge queued by --reset-managed-namespaces rather than by
|
||||||
|
// stale computation over the manifest.
|
||||||
|
Reset bool
|
||||||
|
// Unrecognized marks a reset purge whose topic the manifest has no record
|
||||||
|
// of writing.
|
||||||
|
Unrecognized bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type wikiNamespacesDocument struct {
|
type wikiNamespacesDocument struct {
|
||||||
@@ -116,7 +136,7 @@ type wikiNamespaceDeclaration struct {
|
|||||||
EditPolicy string `json:"edit_policy" yaml:"edit_policy"`
|
EditPolicy string `json:"edit_policy" yaml:"edit_policy"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress func(string)) (deployResult, error) {
|
func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress func(string)) (DeployResult, error) {
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(string) {}
|
progress = func(string) {}
|
||||||
}
|
}
|
||||||
@@ -132,19 +152,19 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(opts.SourceDir); err != nil {
|
if _, err := os.Stat(opts.SourceDir); err != nil {
|
||||||
return deployResult{}, fmt.Errorf("wiki source directory not found: %w", err)
|
return DeployResult{}, fmt.Errorf("wiki source directory not found: %w", err)
|
||||||
}
|
}
|
||||||
if opts.Endpoint == "" {
|
if opts.Endpoint == "" {
|
||||||
return deployResult{}, errors.New("NODEBB_API_ENDPOINT is required")
|
return DeployResult{}, errors.New("NODEBB_API_ENDPOINT is required")
|
||||||
}
|
}
|
||||||
if opts.Token == "" {
|
if opts.Token == "" {
|
||||||
return deployResult{}, errors.New("NODEBB_API_TOKEN is required")
|
return DeployResult{}, errors.New("NODEBB_API_TOKEN is required")
|
||||||
}
|
}
|
||||||
if opts.Username != "" || opts.Password != "" || opts.NotesDelimiter != "" {
|
if opts.Username != "" || opts.Password != "" || opts.NotesDelimiter != "" {
|
||||||
return deployResult{}, errors.New("DokuWiki deployment options are no longer supported; use NodeBB endpoint, token, and category mappings")
|
return DeployResult{}, errors.New("DokuWiki deployment options are no longer supported; use NodeBB endpoint, token, and category mappings")
|
||||||
}
|
}
|
||||||
if opts.StalePolicy != "" && opts.StalePolicy != "report" && opts.StalePolicy != "archive" && opts.StalePolicy != "purge" {
|
if opts.StalePolicy != "" && opts.StalePolicy != "report" && opts.StalePolicy != "archive" && opts.StalePolicy != "purge" {
|
||||||
return deployResult{}, fmt.Errorf("wiki stale policy %q is not supported", opts.StalePolicy)
|
return DeployResult{}, fmt.Errorf("wiki stale policy %q is not supported", opts.StalePolicy)
|
||||||
}
|
}
|
||||||
if opts.TitlePrefixMinLength <= 0 {
|
if opts.TitlePrefixMinLength <= 0 {
|
||||||
opts.TitlePrefixMinLength = p.EffectiveConfig().TopData.Wiki.TitlePrefixMinLength
|
opts.TitlePrefixMinLength = p.EffectiveConfig().TopData.Wiki.TitlePrefixMinLength
|
||||||
@@ -154,7 +174,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
if len(namespaces) == 0 {
|
if len(namespaces) == 0 {
|
||||||
declarations, err := loadWikiNamespaceDeclarations(p)
|
declarations, err := loadWikiNamespaceDeclarations(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return deployResult{}, err
|
return DeployResult{}, err
|
||||||
}
|
}
|
||||||
for _, declaration := range declarations {
|
for _, declaration := range declarations {
|
||||||
namespaces = append(namespaces, declaration.ID)
|
namespaces = append(namespaces, declaration.ID)
|
||||||
@@ -164,7 +184,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
}
|
}
|
||||||
envCategories, err := categoryIDsFromNamespaceEnv(declarations)
|
envCategories, err := categoryIDsFromNamespaceEnv(declarations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return deployResult{}, err
|
return DeployResult{}, err
|
||||||
}
|
}
|
||||||
if len(envCategories) > 0 {
|
if len(envCategories) > 0 {
|
||||||
merged := envCategories
|
merged := envCategories
|
||||||
@@ -183,7 +203,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
progress(fmt.Sprintf("Collecting local wiki pages from %s", opts.SourceDir))
|
progress(fmt.Sprintf("Collecting local wiki pages from %s", opts.SourceDir))
|
||||||
pages, err := collectLocalPages(opts.SourceDir, opts.PageIndexPath, namespaces)
|
pages, err := collectLocalPages(opts.SourceDir, opts.PageIndexPath, namespaces)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return deployResult{}, err
|
return DeployResult{}, err
|
||||||
}
|
}
|
||||||
progress(fmt.Sprintf("Loaded %d local wiki page(s)", len(pages)))
|
progress(fmt.Sprintf("Loaded %d local wiki page(s)", len(pages)))
|
||||||
manifest := loadDeployManifest(manifestPath)
|
manifest := loadDeployManifest(manifestPath)
|
||||||
@@ -193,7 +213,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
progress(fmt.Sprintf("Planning NodeBB wiki deploy for %d local page(s)", len(pages)))
|
progress(fmt.Sprintf("Planning NodeBB wiki deploy for %d local page(s)", len(pages)))
|
||||||
plans, result, nextManifest, err := planNodeBBDeploy(pages, manifest, opts, client, progress)
|
plans, result, nextManifest, err := planNodeBBDeploy(pages, manifest, opts, client, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return deployResult{}, err
|
return DeployResult{}, err
|
||||||
}
|
}
|
||||||
result.LocalPages = len(pages)
|
result.LocalPages = len(pages)
|
||||||
result.Manifest = manifestPath
|
result.Manifest = manifestPath
|
||||||
@@ -212,6 +232,8 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
}
|
}
|
||||||
orderedPlans := orderNodeBBDeployPlans(plans)
|
orderedPlans := orderNodeBBDeployPlans(plans)
|
||||||
progress(fmt.Sprintf("Executing NodeBB wiki actions: total %d, create %d, update %d, rename %d, archive %d, purge %d", len(orderedPlans), result.Created, result.Updated, result.Renamed, result.Archived, result.Purged))
|
progress(fmt.Sprintf("Executing NodeBB wiki actions: total %d, create %d, update %d, rename %d, archive %d, purge %d", len(orderedPlans), result.Created, result.Updated, result.Renamed, result.Archived, result.Purged))
|
||||||
|
resetPurgedByCID := map[int]int{}
|
||||||
|
resetSkippedByCID := map[int]int{}
|
||||||
for i, plan := range orderedPlans {
|
for i, plan := range orderedPlans {
|
||||||
if shouldReportNodeBBDeployActionProgress(i, len(orderedPlans)) {
|
if shouldReportNodeBBDeployActionProgress(i, len(orderedPlans)) {
|
||||||
progress(fmt.Sprintf("Executing NodeBB wiki action %d/%d: %s %s", i+1, len(orderedPlans), plan.Action, plan.Page.PageID))
|
progress(fmt.Sprintf("Executing NodeBB wiki action %d/%d: %s %s", i+1, len(orderedPlans), plan.Action, plan.Page.PageID))
|
||||||
@@ -242,19 +264,52 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
|||||||
return result, fmt.Errorf("deploy wiki page %q: update NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
|
return result, fmt.Errorf("deploy wiki page %q: update NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
|
||||||
}
|
}
|
||||||
case "archive":
|
case "archive":
|
||||||
|
// Archiving rewrites the page rather than removing it, so it goes
|
||||||
|
// through the ordinary post edit the wiki plugin allows; only
|
||||||
|
// delete, restore, and purge are reserved to the page actions.
|
||||||
if err := client.updatePost(plan.Entry.TID, plan.Entry.PID, plan.Content, summary); err != nil {
|
if err := client.updatePost(plan.Entry.TID, plan.Entry.PID, plan.Content, summary); err != nil {
|
||||||
return result, fmt.Errorf("deploy wiki page %q: archive NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
|
return result, fmt.Errorf("deploy wiki page %q: archive NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
|
||||||
}
|
}
|
||||||
case "purge":
|
case "purge":
|
||||||
if err := client.purgeTopic(plan.Entry.TID); err != nil {
|
if err := client.purgeWikiPage(plan.Entry.TID); err != nil {
|
||||||
|
// A namespace reset sweeps every topic in the category, so it
|
||||||
|
// can reach pages NodeBB will not delete at all — the wiki home
|
||||||
|
// topic above all. Skip those rather than abandoning the reset.
|
||||||
|
if plan.Reset && isNodeBBWikiPageUndeletable(err) {
|
||||||
|
result.ResetSkipped++
|
||||||
|
result.ResetPurged--
|
||||||
|
result.Purged--
|
||||||
|
result.Stale--
|
||||||
|
if plan.Unrecognized {
|
||||||
|
result.ResetUnrecognized--
|
||||||
|
}
|
||||||
|
resetSkippedByCID[plan.Entry.CID]++
|
||||||
|
progress(fmt.Sprintf("NodeBB refused to delete wiki topic %d during managed namespace reset; skipping it", plan.Entry.TID))
|
||||||
|
continue
|
||||||
|
}
|
||||||
return result, fmt.Errorf("deploy wiki page %q: purge NodeBB topic %d: %w", plan.Page.PageID, plan.Entry.TID, err)
|
return result, fmt.Errorf("deploy wiki page %q: purge NodeBB topic %d: %w", plan.Page.PageID, plan.Entry.TID, err)
|
||||||
}
|
}
|
||||||
|
if plan.Reset {
|
||||||
|
resetPurgedByCID[plan.Entry.CID]++
|
||||||
|
}
|
||||||
case "rename":
|
case "rename":
|
||||||
if err := client.renameWikiPage(plan.Entry.TID, plan.Entry.CID, plan.Title); err != nil {
|
if err := client.renameWikiPage(plan.Entry.TID, plan.Entry.CID, plan.Title); err != nil {
|
||||||
return result, fmt.Errorf("deploy wiki page %q: rename NodeBB topic %d to %q: %w", plan.Page.PageID, plan.Entry.TID, plan.Title, err)
|
return result, fmt.Errorf("deploy wiki page %q: rename NodeBB topic %d to %q: %w", plan.Page.PageID, plan.Entry.TID, plan.Title, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// NodeBB answers 403 both for the pages it will never delete — the wiki
|
||||||
|
// home topic — and for a token without purge privileges, and the two are
|
||||||
|
// not distinguishable from the response. What tells them apart is scope: a
|
||||||
|
// category where nothing at all could be deleted is a privilege problem,
|
||||||
|
// not a home page. Reporting that as a completed reset would leave the
|
||||||
|
// manifest claiming a fresh start over pages that are all still there, so
|
||||||
|
// the next deploy would recreate every one of them as a duplicate.
|
||||||
|
for _, cid := range slices.Sorted(maps.Keys(resetSkippedByCID)) {
|
||||||
|
if resetPurgedByCID[cid] == 0 {
|
||||||
|
return result, fmt.Errorf("NodeBB refused every managed namespace reset deletion in category %d (%d topic(s)); check that the deploy token has wiki purge privileges there", cid, resetSkippedByCID[cid])
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := saveDeployManifest(manifestPath, nextManifest); err != nil {
|
if err := saveDeployManifest(manifestPath, nextManifest); err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -486,7 +541,7 @@ func isDir(path string) bool {
|
|||||||
return err == nil && info.IsDir()
|
return err == nil && info.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManifest, opts DeployWikiOptions, client *nodeBBClient, progress func(string)) ([]wikiDeployPlan, deployResult, wikiDeployManifest, error) {
|
func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManifest, opts DeployWikiOptions, client *nodeBBClient, progress func(string)) ([]wikiDeployPlan, DeployResult, wikiDeployManifest, error) {
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(string) {}
|
progress = func(string) {}
|
||||||
}
|
}
|
||||||
@@ -494,17 +549,23 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
|
|||||||
next := wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
next := wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
||||||
remotePagesByCID := map[int][]nodeBBWikiPage{}
|
remotePagesByCID := map[int][]nodeBBWikiPage{}
|
||||||
var plans []wikiDeployPlan
|
var plans []wikiDeployPlan
|
||||||
var result deployResult
|
var result DeployResult
|
||||||
if opts.ResetManagedNamespaces {
|
if opts.ResetManagedNamespaces {
|
||||||
if !opts.AllowCreates && len(pageIDs) > 0 {
|
if !opts.AllowCreates && len(pageIDs) > 0 {
|
||||||
return nil, result, next, errors.New("wiki managed namespace reset requires --create so current generated pages can be recreated")
|
return nil, result, next, errors.New("wiki managed namespace reset requires --create so current generated pages can be recreated")
|
||||||
}
|
}
|
||||||
resetPlans, purged, err := planManagedNamespaceReset(opts, client, progress)
|
resetPlans, purged, unrecognized, err := planManagedNamespaceReset(opts, manifest, client, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, result, next, err
|
return nil, result, next, err
|
||||||
}
|
}
|
||||||
plans = append(plans, resetPlans...)
|
plans = append(plans, resetPlans...)
|
||||||
result.Purged += purged
|
result.Purged += purged
|
||||||
|
// A namespace reset deletes remote pages the same way stale purge does,
|
||||||
|
// so it is counted as stale: that is the number callers word their
|
||||||
|
// destructive-policy warning around.
|
||||||
|
result.Stale += purged
|
||||||
|
result.ResetPurged = purged
|
||||||
|
result.ResetUnrecognized = unrecognized
|
||||||
manifest = wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
manifest = wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
||||||
for _, cid := range opts.CategoryIDs {
|
for _, cid := range opts.CategoryIDs {
|
||||||
if cid != 0 {
|
if cid != 0 {
|
||||||
@@ -682,7 +743,11 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
|
|||||||
return plans, result, next, nil
|
return plans, result, next, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func planManagedNamespaceReset(opts DeployWikiOptions, client *nodeBBClient, progress func(string)) ([]wikiDeployPlan, int, error) {
|
// planManagedNamespaceReset queues a purge for every topic in each managed
|
||||||
|
// category, not only the ones the manifest says we wrote. The second return is
|
||||||
|
// the plan count; the third is how many of those topics the manifest has no
|
||||||
|
// record of, which is the subset a re-seed cannot put back.
|
||||||
|
func planManagedNamespaceReset(opts DeployWikiOptions, manifest wikiDeployManifest, client *nodeBBClient, progress func(string)) ([]wikiDeployPlan, int, int, error) {
|
||||||
namespaces := slices.Clone(opts.Namespaces)
|
namespaces := slices.Clone(opts.Namespaces)
|
||||||
if len(namespaces) == 0 {
|
if len(namespaces) == 0 {
|
||||||
for namespace := range opts.CategoryIDs {
|
for namespace := range opts.CategoryIDs {
|
||||||
@@ -691,7 +756,15 @@ func planManagedNamespaceReset(opts DeployWikiOptions, client *nodeBBClient, pro
|
|||||||
}
|
}
|
||||||
slices.Sort(namespaces)
|
slices.Sort(namespaces)
|
||||||
|
|
||||||
|
manifestTIDs := map[int]struct{}{}
|
||||||
|
for _, entry := range manifest.Pages {
|
||||||
|
if entry.TID != 0 {
|
||||||
|
manifestTIDs[entry.TID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
seenTIDs := map[int]struct{}{}
|
seenTIDs := map[int]struct{}{}
|
||||||
|
unrecognized := 0
|
||||||
var plans []wikiDeployPlan
|
var plans []wikiDeployPlan
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range namespaces {
|
||||||
cid := opts.CategoryIDs[namespace]
|
cid := opts.CategoryIDs[namespace]
|
||||||
@@ -701,7 +774,7 @@ func planManagedNamespaceReset(opts DeployWikiOptions, client *nodeBBClient, pro
|
|||||||
progress(fmt.Sprintf("Listing NodeBB wiki namespace category %d for managed reset", cid))
|
progress(fmt.Sprintf("Listing NodeBB wiki namespace category %d for managed reset", cid))
|
||||||
remotePages, err := client.listNamespacePages(cid)
|
remotePages, err := client.listNamespacePages(cid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, 0, err
|
||||||
}
|
}
|
||||||
slices.SortFunc(remotePages, func(a, b nodeBBWikiPage) int {
|
slices.SortFunc(remotePages, func(a, b nodeBBWikiPage) int {
|
||||||
return a.TID - b.TID
|
return a.TID - b.TID
|
||||||
@@ -714,19 +787,25 @@ func planManagedNamespaceReset(opts DeployWikiOptions, client *nodeBBClient, pro
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seenTIDs[remotePage.TID] = struct{}{}
|
seenTIDs[remotePage.TID] = struct{}{}
|
||||||
|
_, known := manifestTIDs[remotePage.TID]
|
||||||
|
if !known {
|
||||||
|
unrecognized++
|
||||||
|
}
|
||||||
title := strings.TrimSpace(remotePage.Title)
|
title := strings.TrimSpace(remotePage.Title)
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = strings.TrimSpace(remotePage.TitleLeaf)
|
title = strings.TrimSpace(remotePage.TitleLeaf)
|
||||||
}
|
}
|
||||||
pageID := fmt.Sprintf("%s:reset-topic-%d", namespace, remotePage.TID)
|
pageID := fmt.Sprintf("%s:reset-topic-%d", namespace, remotePage.TID)
|
||||||
plans = append(plans, wikiDeployPlan{
|
plans = append(plans, wikiDeployPlan{
|
||||||
Page: wikiDeployPage{PageID: pageID, Title: title, Namespace: namespace},
|
Page: wikiDeployPage{PageID: pageID, Title: title, Namespace: namespace},
|
||||||
Entry: wikiDeployManifestPage{TID: remotePage.TID, CID: cid, Namespace: namespace, Title: title},
|
Entry: wikiDeployManifestPage{TID: remotePage.TID, CID: cid, Namespace: namespace, Title: title},
|
||||||
Action: "purge",
|
Action: "purge",
|
||||||
|
Reset: true,
|
||||||
|
Unrecognized: !known,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return plans, len(plans), nil
|
return plans, len(plans), unrecognized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func findMappedRemoteTopic(entry wikiDeployManifestPage, client *nodeBBClient) (nodeBBPost, bool, error) {
|
func findMappedRemoteTopic(entry wikiDeployManifestPage, client *nodeBBClient) (nodeBBPost, bool, error) {
|
||||||
@@ -1611,16 +1690,36 @@ func (c *nodeBBClient) renameWikiPage(tid, cid int, title string) error {
|
|||||||
return c.request("PUT", "/api/v3/plugins/westgate-wiki/page/move", body, nil)
|
return c.request("PUT", "/api/v3/plugins/westgate-wiki/page/move", body, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *nodeBBClient) purgeTopic(tid int) error {
|
// purgeWikiPage deletes a wiki topic through the wiki plugin's own page
|
||||||
|
// actions. The core DELETE /api/v3/topics/{tid} route is refused for topics in
|
||||||
|
// wiki categories, because revision history is plugin-owned, so a purge has to
|
||||||
|
// tombstone the page first and then hard-purge it.
|
||||||
|
func (c *nodeBBClient) purgeWikiPage(tid int) error {
|
||||||
if tid == 0 {
|
if tid == 0 {
|
||||||
return fmt.Errorf("NodeBB topic purge requires topic id")
|
return fmt.Errorf("NodeBB wiki page purge requires topic id")
|
||||||
}
|
}
|
||||||
err := c.request(http.MethodDelete, fmt.Sprintf("/api/v3/topics/%d", tid), nil, nil)
|
body := map[string]any{"tid": tid}
|
||||||
|
if err := c.request(http.MethodPut, "/api/v3/plugins/westgate-wiki/page/tombstone", body, nil); err != nil {
|
||||||
|
if isNodeBBMissingResource(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := c.request(http.MethodDelete, "/api/v3/plugins/westgate-wiki/page/hard-purge", body, nil); err != nil {
|
||||||
|
if isNodeBBMissingResource(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isNodeBBWikiPageUndeletable reports whether NodeBB refused to delete the page
|
||||||
|
// outright rather than failing transiently. The wiki home topic answers this
|
||||||
|
// way: the plugin excludes it from tombstone, restore, and purge alike.
|
||||||
|
func isNodeBBWikiPageUndeletable(err error) bool {
|
||||||
var httpErr nodeBBHTTPError
|
var httpErr nodeBBHTTPError
|
||||||
if errors.As(err, &httpErr) && (httpErr.Status == http.StatusNotFound || httpErr.Status == http.StatusGone) {
|
return errors.As(err, &httpErr) && httpErr.Status == http.StatusForbidden
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *nodeBBClient) acquireEditLock(tid int) (nodeBBEditLock, error) {
|
func (c *nodeBBClient) acquireEditLock(tid int) (nodeBBEditLock, error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package topdata
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
@@ -35,7 +36,7 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCalls := 0
|
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" {
|
if got := r.Header.Get("Authorization"); got != "Bearer nodebb-token" {
|
||||||
t.Fatalf("unexpected authorization header %q", got)
|
t.Fatalf("unexpected authorization header %q", got)
|
||||||
}
|
}
|
||||||
@@ -50,7 +51,7 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -92,7 +93,7 @@ func TestDeployWikiReportsPlanningProgressBeforeRemoteWork(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress := []string{}
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -100,7 +101,7 @@ func TestDeployWikiReportsPlanningProgressBeforeRemoteWork(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -174,7 +175,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
progress := []string{}
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -194,7 +195,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -228,7 +229,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
|||||||
|
|
||||||
func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *testing.T) {
|
func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *testing.T) {
|
||||||
requests := 0
|
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++
|
requests++
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
@@ -240,7 +241,7 @@ func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *te
|
|||||||
"nextCursor": "same-cursor",
|
"nextCursor": "same-cursor",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := newNodeBBClient(server.URL, "nodebb-token")
|
client := newNodeBBClient(server.URL, "nodebb-token")
|
||||||
@@ -258,7 +259,7 @@ func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *te
|
|||||||
|
|
||||||
func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T) {
|
func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T) {
|
||||||
requests := 0
|
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++
|
requests++
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
pages := []map[string]any{
|
pages := []map[string]any{
|
||||||
@@ -276,7 +277,7 @@ func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T
|
|||||||
"nextCursor": "same-cursor",
|
"nextCursor": "same-cursor",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := newNodeBBClient(server.URL, "nodebb-token")
|
client := newNodeBBClient(server.URL, "nodebb-token")
|
||||||
@@ -480,7 +481,7 @@ func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) {
|
|||||||
t.Fatalf("write deploy manifest: %v", err)
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -508,7 +509,7 @@ func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -731,7 +732,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createCalls := 0
|
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" {
|
if got := r.Header.Get("Authorization"); got != "Bearer nodebb-token" {
|
||||||
t.Fatalf("unexpected authorization header %q", got)
|
t.Fatalf("unexpected authorization header %q", got)
|
||||||
}
|
}
|
||||||
@@ -769,7 +770,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||||
@@ -848,7 +849,7 @@ func TestDeployWikiRepairsManifestedPageMissingSourceContentSync(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCalls := 0
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -876,7 +877,7 @@ func TestDeployWikiRepairsManifestedPageMissingSourceContentSync(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -951,7 +952,7 @@ func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitl
|
|||||||
t.Fatalf("write source page: %v", err)
|
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" {
|
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/5/pages" {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||||
@@ -983,7 +984,7 @@ func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitl
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1010,7 +1011,7 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMi
|
|||||||
t.Fatalf("write source page: %v", err)
|
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" {
|
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/5/pages" {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||||
@@ -1038,7 +1039,7 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMi
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1080,7 +1081,7 @@ func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.
|
|||||||
}
|
}
|
||||||
|
|
||||||
renameCalls := 0
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1109,7 +1110,7 @@ func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1166,7 +1167,7 @@ func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renameCalls := 0
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1195,7 +1196,7 @@ func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1258,9 +1259,9 @@ func TestDeployWikiDoesNotRenameHeadinglessPageToPageIDFallback(t *testing.T) {
|
|||||||
t.Fatalf("write deploy manifest: %v", err)
|
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())
|
t.Fatalf("headingless unchanged page must not call NodeBB, got %s %s", r.Method, r.URL.String())
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1320,7 +1321,7 @@ func TestDeployWikiRenamesBrokenHeadinglessFallbackTitleBackToPageIndexTitle(t *
|
|||||||
}
|
}
|
||||||
|
|
||||||
renameCalls := 0
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/topics/7":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1345,7 +1346,7 @@ func TestDeployWikiRenamesBrokenHeadinglessFallbackTitleBackToPageIndexTitle(t *
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1396,7 +1397,7 @@ func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t
|
|||||||
|
|
||||||
createCalls := 0
|
createCalls := 0
|
||||||
var updated string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/9/pages":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/9/pages":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1442,7 +1443,7 @@ func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||||
@@ -1509,7 +1510,7 @@ func TestDeployWikiMergesHTMLManagedAndManualRegions(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var updated string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1533,7 +1534,7 @@ func TestDeployWikiMergesHTMLManagedAndManualRegions(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1589,7 +1590,7 @@ func TestDeployWikiUpdateAcquiresWestgateWikiEditLock(t *testing.T) {
|
|||||||
|
|
||||||
lockAcquired := false
|
lockAcquired := false
|
||||||
var updateToken string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1621,7 +1622,7 @@ func TestDeployWikiUpdateAcquiresWestgateWikiEditLock(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1668,7 +1669,7 @@ func TestDeployWikiCreateCollisionAdoptsExistingNodeBBPage(t *testing.T) {
|
|||||||
|
|
||||||
createCalls := 0
|
createCalls := 0
|
||||||
var updated string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/9/pages" && r.URL.Query().Get("q") == "":
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1719,7 +1720,7 @@ func TestDeployWikiCreateCollisionAdoptsExistingNodeBBPage(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||||
@@ -1771,7 +1772,7 @@ func TestDeployWikiCreateCollisionSearchesCanonicalTitleSegment(t *testing.T) {
|
|||||||
|
|
||||||
createCalls := 0
|
createCalls := 0
|
||||||
var updated string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/58/pages" && r.URL.Query().Get("q") == "":
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -1828,7 +1829,7 @@ func TestDeployWikiCreateCollisionSearchesCanonicalTitleSegment(t *testing.T) {
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||||
@@ -1868,7 +1869,7 @@ func TestDeployWikiReportsAndArchivesStalePages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var archived string
|
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" {
|
if r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/edit-lock" {
|
||||||
respondWikiEditLock(t, w, r, 7, "archive-lock")
|
respondWikiEditLock(t, w, r, 7, "archive-lock")
|
||||||
return
|
return
|
||||||
@@ -1889,7 +1890,7 @@ func TestDeployWikiReportsAndArchivesStalePages(t *testing.T) {
|
|||||||
archived = req.Content
|
archived = req.Content
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pid": 42, "tid": 7}})
|
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pid": 42, "tid": 7}})
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
report, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
report, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1945,9 +1946,9 @@ func TestDeployWikiDryRunPlansTrackedStalePagePurge(t *testing.T) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("write deploy manifest: %v", err)
|
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)
|
t.Fatalf("dry-run purge must not call NodeBB, got %s %s", r.Method, r.URL.Path)
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -1984,9 +1985,9 @@ func TestDeployWikiPurgeRefusesStaleManifestEntryWithoutTopicID(t *testing.T) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("write deploy manifest: %v", err)
|
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)
|
t.Fatalf("missing-topic-id purge must not call NodeBB, got %s %s", r.Method, r.URL.Path)
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -2017,15 +2018,12 @@ func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("write deploy manifest: %v", err)
|
t.Fatalf("write deploy manifest: %v", err)
|
||||||
}
|
}
|
||||||
purgeCalls := 0
|
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 r.Method != http.MethodDelete || r.URL.Path != "/api/v3/topics/7" {
|
if !answerWikiPagePurge(t, w, r, &calls) {
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
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()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -2041,8 +2039,8 @@ func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
|||||||
if result.Stale != 1 || result.Purged != 1 {
|
if result.Stale != 1 || result.Purged != 1 {
|
||||||
t.Fatalf("expected one stale purge, got %#v", result)
|
t.Fatalf("expected one stale purge, got %#v", result)
|
||||||
}
|
}
|
||||||
if purgeCalls != 1 {
|
if strings.Join(calls, ",") != "tombstone:7,hard-purge:7" {
|
||||||
t.Fatalf("expected one NodeBB topic purge call, got %d", purgeCalls)
|
t.Fatalf("expected the wiki plugin page actions to purge topic 7, got %#v", calls)
|
||||||
}
|
}
|
||||||
if _, ok := loadDeployManifest(manifestPath).Pages["skills:retired"]; ok {
|
if _, ok := loadDeployManifest(manifestPath).Pages["skills:retired"]; ok {
|
||||||
t.Fatalf("expected purged stale manifest entry to be removed")
|
t.Fatalf("expected purged stale manifest entry to be removed")
|
||||||
@@ -2075,18 +2073,17 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
|||||||
}
|
}
|
||||||
|
|
||||||
var calls []string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
_ = 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":
|
case r.Method == http.MethodPost && r.URL.Path == "/api/v3/topics":
|
||||||
calls = append(calls, "create")
|
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)
|
t.Fatalf("expected stale purge before create, got calls %#v", calls)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -2102,7 +2099,7 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -2120,7 +2117,7 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
|||||||
if result.Created != 1 || result.Purged != 1 {
|
if result.Created != 1 || result.Purged != 1 {
|
||||||
t.Fatalf("expected one create and one purge, got %#v", result)
|
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)
|
t.Fatalf("expected purge before create, got %#v", calls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2150,8 +2147,12 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
|||||||
t.Fatalf("write deploy manifest: %v", err)
|
t.Fatalf("write deploy manifest: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resetCalls = "tombstone:7,hard-purge:7,tombstone:8,hard-purge:8"
|
||||||
var calls []string
|
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 {
|
switch {
|
||||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/3/pages":
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -2164,17 +2165,9 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
|||||||
"hasMore": false,
|
"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":
|
case r.Method == http.MethodPost && r.URL.Path == "/api/v3/topics":
|
||||||
calls = append(calls, "create")
|
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)
|
t.Fatalf("expected namespace reset purges before create, got %#v", calls)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -2190,7 +2183,7 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
|||||||
default:
|
default:
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -2205,10 +2198,21 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("DeployWikiWithOptions reset-managed-namespaces failed: %v", err)
|
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)
|
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)
|
t.Fatalf("expected reset purges before create, got %#v", calls)
|
||||||
}
|
}
|
||||||
manifest := loadDeployManifest(manifestPath)
|
manifest := loadDeployManifest(manifestPath)
|
||||||
@@ -2236,9 +2240,9 @@ func TestDeployWikiResetManagedNamespacesRequiresCreateForLocalPages(t *testing.
|
|||||||
t.Fatalf("write source page: %v", err)
|
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())
|
t.Fatalf("reset without --create must fail before remote calls, got %s %s", r.Method, r.URL.String())
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -2270,21 +2274,21 @@ func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T)
|
|||||||
t.Fatalf("write deploy manifest: %v", err)
|
t.Fatalf("write deploy manifest: %v", err)
|
||||||
}
|
}
|
||||||
seen := map[string]int{}
|
seen := map[string]int{}
|
||||||
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.MethodDelete {
|
call, ok := wikiPagePurgeCall(t, r)
|
||||||
|
if !ok {
|
||||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
}
|
}
|
||||||
seen[r.URL.Path]++
|
seen[call]++
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
switch r.URL.Path {
|
// Topic 7 is already gone, so the plugin cannot find the page to
|
||||||
case "/api/v3/topics/7":
|
// tombstone; topic 8 still exists and purges normally.
|
||||||
|
if call == "tombstone:7" {
|
||||||
http.Error(w, `{"status":{"message":"topic not found"}}`, http.StatusNotFound)
|
http.Error(w, `{"status":{"message":"topic not found"}}`, http.StatusNotFound)
|
||||||
case "/api/v3/topics/8":
|
return
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
|
||||||
default:
|
|
||||||
t.Fatalf("unexpected purge path %s", r.URL.Path)
|
|
||||||
}
|
}
|
||||||
}))
|
_ = json.NewEncoder(w).Encode(map[string]any{"status": map[string]any{"code": "ok"}})
|
||||||
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||||
@@ -2300,8 +2304,11 @@ func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T)
|
|||||||
if result.Stale != 2 || result.Purged != 2 {
|
if result.Stale != 2 || result.Purged != 2 {
|
||||||
t.Fatalf("expected two stale purges, got %#v", result)
|
t.Fatalf("expected two stale purges, got %#v", result)
|
||||||
}
|
}
|
||||||
if seen["/api/v3/topics/7"] != 1 || seen["/api/v3/topics/8"] != 1 {
|
if seen["tombstone:7"] != 1 || seen["hard-purge:7"] != 0 {
|
||||||
t.Fatalf("expected one purge call for each tracked topic, got %#v", seen)
|
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)
|
manifest := loadDeployManifest(manifestPath)
|
||||||
if _, ok := manifest.Pages["skills:retired"]; ok {
|
if _, ok := manifest.Pages["skills:retired"]; ok {
|
||||||
@@ -2339,9 +2346,9 @@ func TestDeployWikiPurgeDoesNotTargetCurrentGeneratedPages(t *testing.T) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatalf("write deploy manifest: %v", err)
|
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)
|
t.Fatalf("current generated page must not call NodeBB during matching-hash deploy, got %s %s", r.Method, r.URL.Path)
|
||||||
}))
|
})
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
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")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"status": "ok", "tid": expectedTID, "token": token}})
|
_ = 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