Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f958436238 |
@@ -11,12 +11,6 @@ permissions:
|
||||
code: read
|
||||
releases: write
|
||||
|
||||
# The shared runner runs 2 jobs at once now (sow-platform#189); capacity 1 was
|
||||
# the implicit mutex. Serialize this workflow explicitly; never cancel mid-run.
|
||||
concurrency:
|
||||
group: build-binaries
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-binaries:
|
||||
runs-on: nix-docker
|
||||
|
||||
@@ -7,12 +7,6 @@ on:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
# One run per ref: a new push obsoletes the run on the old head, so cancel it
|
||||
# and free the runner slot for the run that can still matter.
|
||||
concurrency:
|
||||
group: ci-${{ gitea.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: nix-docker
|
||||
|
||||
@@ -16,12 +16,6 @@ on:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
# The shared runner runs 2 jobs at once now (sow-platform#189); capacity 1 was
|
||||
# the implicit mutex. Serialize this workflow explicitly; never cancel mid-run.
|
||||
concurrency:
|
||||
group: sync-wrappers
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: nix-docker
|
||||
|
||||
@@ -57,32 +57,14 @@ preference — move it.
|
||||
so any non-interactive shell (every agent) hangs forever without
|
||||
`</dev/null`. Same trap on `tea issues create --description` and
|
||||
`tea pr create`.
|
||||
- **Apply / remove labels**: `tea issues edit <number> --add-labels "Kind/Bug"`
|
||||
(and `--remove-labels`). This handles org-level labels (`Kind/*`,
|
||||
`Priority/*`, `Reviewed/*`, `Status/*`) from tea 0.15 onwards. On 0.14 it did
|
||||
not: name resolution searched only this repo's own label set, so an org label
|
||||
matched nothing and the command exited 0, printed the issue, and changed
|
||||
nothing. Upstream fixed it in v0.15 (`modules/task/labels.go` also queries
|
||||
`ListOrgLabels`). Note `tea labels` lists repo labels only and will not show
|
||||
you the org set — `tea api orgs/ShadowsOverWestgate/labels` does.
|
||||
- **`tea api` needs a token in the login; SSH auth is not enough.** It sends
|
||||
only the login's `token:` field and does not sign requests with your SSH key,
|
||||
so an SSH-key-only login gets `{"message":"token is required"}` on every call
|
||||
that needs auth. Reads against these public repos still succeed anonymously,
|
||||
which hides the gap until the first write. Add a token to the login in
|
||||
`~/.config/tea/config.yml` (Settings > Applications; `write:issue` covers
|
||||
labels, comments and dependencies) and `tea api` works for reads and writes
|
||||
alike. The `tea issues` / `tea pr` subcommands authenticate either way, so
|
||||
they keep working with no token at all — that asymmetry is what makes this
|
||||
confusing to diagnose.
|
||||
- **Verify every label change by re-reading it.** A label command exiting 0 is
|
||||
not evidence it applied — that is exactly how the 0.14 silent no-op above hid
|
||||
for so long, and assuming otherwise has already cost one investigation
|
||||
several wrong turns. Read the resulting set back with
|
||||
`tea api repos/ShadowsOverWestgate/sow-tools/issues/<number>` and check its
|
||||
`labels` field, or `tea issues ls -o json`. The read-back reflects the write
|
||||
immediately; if it comes back empty, the write genuinely failed. Do not
|
||||
explain an empty read-back away as replication lag.
|
||||
- **Apply / remove labels**: `tea api --method PATCH` on the issue, or
|
||||
`tea api repos/ShadowsOverWestgate/sow-tools/issues/<number>/labels` endpoints.
|
||||
`tea issues edit --add-labels` works too, org-level labels (`Kind/*`,
|
||||
`Priority/*`) included, but a read-back straight afterwards can still show
|
||||
the old set — `tea issues ls` has returned stale labels for several seconds
|
||||
after an edit that succeeded. Never conclude an edit failed from a single
|
||||
immediate list; re-read after a pause, or use the `labels` endpoints above,
|
||||
whose response *is* the resulting label set and needs no second call.
|
||||
- **Close**: `tea issues close <number>`
|
||||
|
||||
`tea` infers the repo from the git remote when run inside the clone.
|
||||
|
||||
@@ -24,19 +24,11 @@ this.
|
||||
tea issues create --title "..." --description "..." --labels "Kind/Bug" </dev/null
|
||||
tea pr create --title "..." --description "..." --labels "Kind/Feature" </dev/null
|
||||
|
||||
# add a label to something that already exists (needs tea >= 0.15 for org labels)
|
||||
tea issues edit <n> --add-labels "Kind/Bug,Priority/High"
|
||||
|
||||
# read the result back — exit 0 is not evidence the label applied
|
||||
tea api "repos/ShadowsOverWestgate/<repo>/issues/<n>"
|
||||
# add a label to something that already exists
|
||||
tea api --method POST "repos/ShadowsOverWestgate/<repo>/issues/<n>/labels" \
|
||||
--data '{"labels":["Kind/Bug","Priority/High"]}'
|
||||
```
|
||||
|
||||
`tea api` sends only the login's `token:` and never your SSH key, so on an
|
||||
SSH-only login every authenticated call fails with
|
||||
`{"message":"token is required"}` while anonymous reads still work. Add a token
|
||||
to `~/.config/tea/config.yml` if you want the raw endpoints. `tea issues edit`
|
||||
needs no token.
|
||||
|
||||
## Kind — what this is (pick exactly one)
|
||||
|
||||
| Label | Use it when |
|
||||
|
||||
+11
-20
@@ -927,28 +927,19 @@ func (c *topdataConsole) emitWikiBuildResult(outputDir string, pageCount int, st
|
||||
fmt.Fprintf(c.stdout, "wiki status: %s\n", status)
|
||||
}
|
||||
|
||||
func (c *topdataConsole) emitWikiDeployResult(result topdata.DeployResult) {
|
||||
func (c *topdataConsole) emitWikiDeployResult(localPages, created, updated, skipped, stale, archived, purged, drifted int, manifest string) {
|
||||
spin.linebreak()
|
||||
fmt.Fprintln(c.stdout, "Deploy Wiki ----------")
|
||||
fmt.Fprintf(c.stdout, "project: %s\n", c.projectName)
|
||||
fmt.Fprintf(c.stdout, "local pages: %d\n", result.LocalPages)
|
||||
fmt.Fprintf(c.stdout, "created: %d\n", result.Created)
|
||||
fmt.Fprintf(c.stdout, "updated: %d\n", result.Updated)
|
||||
fmt.Fprintf(c.stdout, "skipped: %d\n", result.Skipped)
|
||||
fmt.Fprintf(c.stdout, "stale: %d\n", result.Stale)
|
||||
fmt.Fprintf(c.stdout, "archived: %d\n", result.Archived)
|
||||
fmt.Fprintf(c.stdout, "purged: %d\n", result.Purged)
|
||||
fmt.Fprintf(c.stdout, "drifted: %d\n", result.Drifted)
|
||||
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))
|
||||
fmt.Fprintf(c.stdout, "local pages: %d\n", localPages)
|
||||
fmt.Fprintf(c.stdout, "created: %d\n", created)
|
||||
fmt.Fprintf(c.stdout, "updated: %d\n", updated)
|
||||
fmt.Fprintf(c.stdout, "skipped: %d\n", skipped)
|
||||
fmt.Fprintf(c.stdout, "stale: %d\n", stale)
|
||||
fmt.Fprintf(c.stdout, "archived: %d\n", archived)
|
||||
fmt.Fprintf(c.stdout, "purged: %d\n", purged)
|
||||
fmt.Fprintf(c.stdout, "drifted: %d\n", drifted)
|
||||
fmt.Fprintf(c.stdout, "manifest: %s\n", c.relPath(manifest))
|
||||
}
|
||||
|
||||
func (c *topdataConsole) relPath(path string) string {
|
||||
@@ -1588,7 +1579,7 @@ func runDeployWiki(ctx context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
console.emitWikiDeployResult(result)
|
||||
console.emitWikiDeployResult(result.LocalPages, result.Created, result.Updated, result.Skipped, result.Stale, result.Archived, result.Purged, result.Drifted, result.Manifest)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"time"
|
||||
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/pipeline"
|
||||
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/topdata"
|
||||
)
|
||||
|
||||
func TestParseBuildHAKArgsContentAddressedRoot(t *testing.T) {
|
||||
@@ -219,16 +218,7 @@ 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.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",
|
||||
})
|
||||
console.emitWikiDeployResult(10, 1, 2, 3, 4, 5, 6, 0, "/workspace/project/build/wiki/deploy-manifest.json")
|
||||
|
||||
output := stdout.String()
|
||||
if !strings.Contains(output, "NodeBB wiki plan") {
|
||||
@@ -239,53 +229,6 @@ 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) {
|
||||
_, err := parseDeployWikiArgs("deploy-wiki", []string{"--help"})
|
||||
if err == nil || !strings.Contains(err.Error(), "--stale-policy <report|archive|purge>") {
|
||||
|
||||
@@ -289,8 +289,7 @@ var Registry = []Builder{
|
||||
"--dry-run report changes without writing",
|
||||
"--create allow missing pages to be created",
|
||||
"--force update unchanged pages",
|
||||
"--reset-managed-namespaces delete every page in the managed categories, including",
|
||||
" pages this deployer never wrote, then recreate from source",
|
||||
"--reset-managed-namespaces reset managed namespace state",
|
||||
},
|
||||
Aliases: []CommandAlias{{Name: "deploy-wiki"}},
|
||||
},
|
||||
|
||||
+29
-189
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"maps"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -53,7 +52,7 @@ type DeployWikiOptions struct {
|
||||
TitlePrefixMinLength int
|
||||
}
|
||||
|
||||
type DeployResult struct {
|
||||
type deployResult struct {
|
||||
LocalPages int
|
||||
Created int
|
||||
Updated int
|
||||
@@ -62,24 +61,8 @@ type DeployResult struct {
|
||||
Purged int
|
||||
Skipped int
|
||||
Drifted int
|
||||
// DriftedPages names the pages counted in Drifted, in plan order, so an
|
||||
// operator can look at them before deciding whether --force is safe.
|
||||
DriftedPages []string
|
||||
Renamed int
|
||||
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 {
|
||||
@@ -98,12 +81,6 @@ type wikiDeployManifest struct {
|
||||
|
||||
type wikiDeployManifestPage struct {
|
||||
Hash string `json:"hash"`
|
||||
// RemoteHash is the managed-region hash of the post body NodeBB handed back
|
||||
// right after our last write to it. NodeBB owns that body, so this — not
|
||||
// Hash, which is the hash of the text we rendered — is what the next run's
|
||||
// drift check compares against. Empty on manifests written before this
|
||||
// field existed; the drift check falls back to Hash for those.
|
||||
RemoteHash string `json:"remote_hash,omitempty"`
|
||||
LastSeenHash string `json:"last_seen_hash,omitempty"`
|
||||
ArchivedHash string `json:"archived_hash,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
@@ -121,13 +98,8 @@ type wikiDeployPlan struct {
|
||||
Entry wikiDeployManifestPage
|
||||
Action string
|
||||
Content string
|
||||
RemoteHash 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 {
|
||||
@@ -144,7 +116,7 @@ type wikiNamespaceDeclaration struct {
|
||||
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 {
|
||||
progress = func(string) {}
|
||||
}
|
||||
@@ -160,19 +132,19 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
}
|
||||
}
|
||||
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 == "" {
|
||||
return DeployResult{}, errors.New("NODEBB_API_ENDPOINT is required")
|
||||
return deployResult{}, errors.New("NODEBB_API_ENDPOINT is required")
|
||||
}
|
||||
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 != "" {
|
||||
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" {
|
||||
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 {
|
||||
opts.TitlePrefixMinLength = p.EffectiveConfig().TopData.Wiki.TitlePrefixMinLength
|
||||
@@ -182,7 +154,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
if len(namespaces) == 0 {
|
||||
declarations, err := loadWikiNamespaceDeclarations(p)
|
||||
if err != nil {
|
||||
return DeployResult{}, err
|
||||
return deployResult{}, err
|
||||
}
|
||||
for _, declaration := range declarations {
|
||||
namespaces = append(namespaces, declaration.ID)
|
||||
@@ -192,7 +164,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
}
|
||||
envCategories, err := categoryIDsFromNamespaceEnv(declarations)
|
||||
if err != nil {
|
||||
return DeployResult{}, err
|
||||
return deployResult{}, err
|
||||
}
|
||||
if len(envCategories) > 0 {
|
||||
merged := envCategories
|
||||
@@ -211,7 +183,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
progress(fmt.Sprintf("Collecting local wiki pages from %s", opts.SourceDir))
|
||||
pages, err := collectLocalPages(opts.SourceDir, opts.PageIndexPath, namespaces)
|
||||
if err != nil {
|
||||
return DeployResult{}, err
|
||||
return deployResult{}, err
|
||||
}
|
||||
progress(fmt.Sprintf("Loaded %d local wiki page(s)", len(pages)))
|
||||
manifest := loadDeployManifest(manifestPath)
|
||||
@@ -221,7 +193,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
progress(fmt.Sprintf("Planning NodeBB wiki deploy for %d local page(s)", len(pages)))
|
||||
plans, result, nextManifest, err := planNodeBBDeploy(pages, manifest, opts, client, progress)
|
||||
if err != nil {
|
||||
return DeployResult{}, err
|
||||
return deployResult{}, err
|
||||
}
|
||||
result.LocalPages = len(pages)
|
||||
result.Manifest = manifestPath
|
||||
@@ -231,7 +203,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
return result, nil
|
||||
}
|
||||
if result.Drifted > 0 && !opts.Force {
|
||||
return result, fmt.Errorf("remote managed wiki content drifted on %d page(s) (%s); rerun with --force to overwrite", result.Drifted, summarizeDriftedPages(result.DriftedPages))
|
||||
return result, errors.New("remote managed wiki content drifted; rerun with --force to overwrite")
|
||||
}
|
||||
|
||||
summary := p.EffectiveConfig().TopData.Wiki.DeployEditSummary
|
||||
@@ -240,8 +212,6 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
}
|
||||
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))
|
||||
resetPurgedByCID := map[int]int{}
|
||||
resetSkippedByCID := map[int]int{}
|
||||
for i, plan := range 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))
|
||||
@@ -267,63 +237,24 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
|
||||
entry.TID = created.TID
|
||||
entry.PID = created.PID
|
||||
nextManifest.Pages[plan.Page.PageID] = entry
|
||||
if err := recordRemoteHash(nextManifest, plan.Page.PageID, created.PID, client); err != nil {
|
||||
return result, err
|
||||
}
|
||||
case "update":
|
||||
if err := client.updatePost(plan.Entry.TID, plan.Entry.PID, plan.Content, summary); err != nil {
|
||||
return result, fmt.Errorf("deploy wiki page %q: update NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
|
||||
}
|
||||
if err := recordRemoteHash(nextManifest, plan.Page.PageID, plan.Entry.PID, client); err != nil {
|
||||
return result, err
|
||||
}
|
||||
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 {
|
||||
return result, fmt.Errorf("deploy wiki page %q: archive NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
|
||||
}
|
||||
case "purge":
|
||||
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
|
||||
}
|
||||
if err := client.purgeTopic(plan.Entry.TID); err != nil {
|
||||
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":
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
return result, err
|
||||
}
|
||||
@@ -555,7 +486,7 @@ func isDir(path string) bool {
|
||||
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 {
|
||||
progress = func(string) {}
|
||||
}
|
||||
@@ -563,23 +494,17 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
|
||||
next := wikiDeployManifest{Version: "nodebb-v1", Pages: map[string]wikiDeployManifestPage{}}
|
||||
remotePagesByCID := map[int][]nodeBBWikiPage{}
|
||||
var plans []wikiDeployPlan
|
||||
var result DeployResult
|
||||
var result deployResult
|
||||
if opts.ResetManagedNamespaces {
|
||||
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")
|
||||
}
|
||||
resetPlans, purged, unrecognized, err := planManagedNamespaceReset(opts, manifest, client, progress)
|
||||
resetPlans, purged, err := planManagedNamespaceReset(opts, client, progress)
|
||||
if err != nil {
|
||||
return nil, result, next, err
|
||||
}
|
||||
plans = append(plans, resetPlans...)
|
||||
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{}}
|
||||
for _, cid := range opts.CategoryIDs {
|
||||
if cid != 0 {
|
||||
@@ -694,9 +619,8 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
|
||||
next.Pages[pageID] = entry
|
||||
}
|
||||
remoteHash := computeManagedHash(remote.Content)
|
||||
if remoteContentDrifted(manifest.Pages[pageID], remote, remoteHash) {
|
||||
if manifest.Pages[pageID].Hash != "" && remoteHash != manifest.Pages[pageID].Hash {
|
||||
result.Drifted++
|
||||
result.DriftedPages = append(result.DriftedPages, pageID)
|
||||
if !opts.Force {
|
||||
continue
|
||||
}
|
||||
@@ -709,7 +633,7 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
|
||||
result.Updated++
|
||||
entry.SourceContentSynced = true
|
||||
next.Pages[pageID] = entry
|
||||
plans = append(plans, wikiDeployPlan{Page: page, Entry: entry, Action: "update", Content: merged})
|
||||
plans = append(plans, wikiDeployPlan{Page: page, Entry: entry, Action: "update", Content: merged, RemoteHash: remoteHash})
|
||||
}
|
||||
for pageID := range manifest.Pages {
|
||||
if _, ok := pages[pageID]; !ok {
|
||||
@@ -758,11 +682,7 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
|
||||
return plans, result, next, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func planManagedNamespaceReset(opts DeployWikiOptions, client *nodeBBClient, progress func(string)) ([]wikiDeployPlan, int, error) {
|
||||
namespaces := slices.Clone(opts.Namespaces)
|
||||
if len(namespaces) == 0 {
|
||||
for namespace := range opts.CategoryIDs {
|
||||
@@ -771,15 +691,7 @@ func planManagedNamespaceReset(opts DeployWikiOptions, manifest wikiDeployManife
|
||||
}
|
||||
slices.Sort(namespaces)
|
||||
|
||||
manifestTIDs := map[int]struct{}{}
|
||||
for _, entry := range manifest.Pages {
|
||||
if entry.TID != 0 {
|
||||
manifestTIDs[entry.TID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
seenTIDs := map[int]struct{}{}
|
||||
unrecognized := 0
|
||||
var plans []wikiDeployPlan
|
||||
for _, namespace := range namespaces {
|
||||
cid := opts.CategoryIDs[namespace]
|
||||
@@ -789,7 +701,7 @@ func planManagedNamespaceReset(opts DeployWikiOptions, manifest wikiDeployManife
|
||||
progress(fmt.Sprintf("Listing NodeBB wiki namespace category %d for managed reset", cid))
|
||||
remotePages, err := client.listNamespacePages(cid)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
return nil, 0, err
|
||||
}
|
||||
slices.SortFunc(remotePages, func(a, b nodeBBWikiPage) int {
|
||||
return a.TID - b.TID
|
||||
@@ -802,10 +714,6 @@ func planManagedNamespaceReset(opts DeployWikiOptions, manifest wikiDeployManife
|
||||
continue
|
||||
}
|
||||
seenTIDs[remotePage.TID] = struct{}{}
|
||||
_, known := manifestTIDs[remotePage.TID]
|
||||
if !known {
|
||||
unrecognized++
|
||||
}
|
||||
title := strings.TrimSpace(remotePage.Title)
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(remotePage.TitleLeaf)
|
||||
@@ -815,12 +723,10 @@ func planManagedNamespaceReset(opts DeployWikiOptions, manifest wikiDeployManife
|
||||
Page: wikiDeployPage{PageID: pageID, Title: title, Namespace: namespace},
|
||||
Entry: wikiDeployManifestPage{TID: remotePage.TID, CID: cid, Namespace: namespace, Title: title},
|
||||
Action: "purge",
|
||||
Reset: true,
|
||||
Unrecognized: !known,
|
||||
})
|
||||
}
|
||||
}
|
||||
return plans, len(plans), unrecognized, nil
|
||||
return plans, len(plans), nil
|
||||
}
|
||||
|
||||
func findMappedRemoteTopic(entry wikiDeployManifestPage, client *nodeBBClient) (nodeBBPost, bool, error) {
|
||||
@@ -884,9 +790,6 @@ func recoverCreateCollision(plan wikiDeployPlan, manifest wikiDeployManifest, cl
|
||||
entry.Stale = false
|
||||
entry.SourceContentSynced = true
|
||||
manifest.Pages[plan.Page.PageID] = entry
|
||||
if err := recordRemoteHash(manifest, plan.Page.PageID, entry.PID, client); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -1224,49 +1127,6 @@ func saveDeployManifest(path string, manifest wikiDeployManifest) error {
|
||||
return os.WriteFile(path, append(raw, '\n'), 0o644)
|
||||
}
|
||||
|
||||
// summarizeDriftedPages names the drifted pages for the operator, capped so a
|
||||
// mass-drift run does not bury the rest of the CI log.
|
||||
func summarizeDriftedPages(pages []string) string {
|
||||
const maxNamed = 10
|
||||
if len(pages) <= maxNamed {
|
||||
return strings.Join(pages, ", ")
|
||||
}
|
||||
return fmt.Sprintf("%s and %d more", strings.Join(pages[:maxNamed], ", "), len(pages)-maxNamed)
|
||||
}
|
||||
|
||||
// remoteContentDrifted answers the only question the drift guard exists to ask:
|
||||
// did the live page change after we last wrote it? It is not "does the live
|
||||
// page differ from what we just rendered" — NodeBB owns the stored body, and a
|
||||
// deploy that regenerates a page always differs from what is live.
|
||||
func remoteContentDrifted(entry wikiDeployManifestPage, remote nodeBBPost, remoteHash string) bool {
|
||||
// A post with no sourceContent was written before the deployer stored one,
|
||||
// so what comes back is NodeBB's rendered HTML and can never hash-equal
|
||||
// anything we wrote. That is a page awaiting the SourceContentSynced
|
||||
// repair, not a human edit.
|
||||
if remote.SourceContent == "" {
|
||||
return false
|
||||
}
|
||||
if entry.RemoteHash != "" {
|
||||
return remoteHash != entry.RemoteHash
|
||||
}
|
||||
// Manifest written before RemoteHash existed: fall back to the old
|
||||
// comparison rather than declaring every tracked page drifted at once.
|
||||
return entry.Hash != "" && remoteHash != entry.Hash
|
||||
}
|
||||
|
||||
// recordRemoteHash reads a page back after writing it and records what NodeBB
|
||||
// actually stored, so the next run compares remote against remote.
|
||||
func recordRemoteHash(manifest wikiDeployManifest, pageID string, pid int, client *nodeBBClient) error {
|
||||
remote, err := client.getPost(pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deploy wiki page %q: read back NodeBB post %d: %w", pageID, pid, err)
|
||||
}
|
||||
entry := manifest.Pages[pageID]
|
||||
entry.RemoteHash = computeManagedHash(remote.Content)
|
||||
manifest.Pages[pageID] = entry
|
||||
return nil
|
||||
}
|
||||
|
||||
func computeManagedHash(content string) string {
|
||||
managed, ok := extractManagedRegion(content)
|
||||
if !ok {
|
||||
@@ -1751,36 +1611,16 @@ func (c *nodeBBClient) renameWikiPage(tid, cid int, title string) error {
|
||||
return c.request("PUT", "/api/v3/plugins/westgate-wiki/page/move", body, nil)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (c *nodeBBClient) purgeTopic(tid int) error {
|
||||
if tid == 0 {
|
||||
return fmt.Errorf("NodeBB wiki page purge requires topic id")
|
||||
return fmt.Errorf("NodeBB topic purge requires topic id")
|
||||
}
|
||||
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 {
|
||||
err := c.request(http.MethodDelete, fmt.Sprintf("/api/v3/topics/%d", tid), nil, nil)
|
||||
var httpErr nodeBBHTTPError
|
||||
return errors.As(err, &httpErr) && httpErr.Status == http.StatusForbidden
|
||||
if errors.As(err, &httpErr) && (httpErr.Status == http.StatusNotFound || httpErr.Status == http.StatusGone) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *nodeBBClient) acquireEditLock(tid int) (nodeBBEditLock, error) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package topdata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -36,7 +35,7 @@ func TestDeployWikiDryRunDoesNotWriteRemoteOrManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
updateCalls := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer nodebb-token" {
|
||||
t.Fatalf("unexpected authorization header %q", got)
|
||||
}
|
||||
@@ -51,7 +50,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{
|
||||
@@ -93,7 +92,7 @@ func TestDeployWikiReportsPlanningProgressBeforeRemoteWork(t *testing.T) {
|
||||
}
|
||||
|
||||
progress := []string{}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -101,7 +100,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{
|
||||
@@ -175,7 +174,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
||||
}
|
||||
|
||||
progress := []string{}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -195,7 +194,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{
|
||||
@@ -229,7 +228,7 @@ func TestDeployWikiReportsLiveExecutionProgress(t *testing.T) {
|
||||
|
||||
func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *testing.T) {
|
||||
requests := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
@@ -241,7 +240,7 @@ func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *te
|
||||
"nextCursor": "same-cursor",
|
||||
},
|
||||
})
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := newNodeBBClient(server.URL, "nodebb-token")
|
||||
@@ -259,7 +258,7 @@ func TestNodeBBNamespacePaginationStopsWhenRepeatedCursorReturnsNoNewPages(t *te
|
||||
|
||||
func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T) {
|
||||
requests := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requests++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pages := []map[string]any{
|
||||
@@ -277,7 +276,7 @@ func TestNodeBBNamespacePaginationRejectsRepeatedCursorWithNewPages(t *testing.T
|
||||
"nextCursor": "same-cursor",
|
||||
},
|
||||
})
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := newNodeBBClient(server.URL, "nodebb-token")
|
||||
@@ -481,7 +480,7 @@ func TestDeployWikiDryRunReadoptsMissingMappedPost(t *testing.T) {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -509,7 +508,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{
|
||||
@@ -732,7 +731,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
createCalls := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer nodebb-token" {
|
||||
t.Fatalf("unexpected authorization header %q", got)
|
||||
}
|
||||
@@ -741,10 +740,6 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42" {
|
||||
respondWikiPost(w, 42, 11, generated)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v3/topics" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
@@ -774,7 +769,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
@@ -800,9 +795,6 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
|
||||
if !entry.SourceContentSynced {
|
||||
t.Fatalf("expected created manifest entry to record sourceContent sync")
|
||||
}
|
||||
if entry.RemoteHash != computeManagedHash(generated) {
|
||||
t.Fatalf("expected created manifest entry to record the read-back remote hash, got %#v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNodeBBPostPrefersSourceContentForWikiHTML(t *testing.T) {
|
||||
@@ -856,7 +848,7 @@ func TestDeployWikiRepairsManifestedPageMissingSourceContentSync(t *testing.T) {
|
||||
}
|
||||
|
||||
updateCalls := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -884,7 +876,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{
|
||||
@@ -959,16 +951,12 @@ func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitl
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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}})
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42" {
|
||||
respondWikiPost(w, 42, 11, generated)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v3/topics" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
@@ -995,7 +983,7 @@ func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitl
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1022,16 +1010,12 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMi
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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}})
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42" {
|
||||
respondWikiPost(w, 42, 11, generated)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/api/v3/topics" {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
@@ -1054,7 +1038,7 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMi
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -1096,7 +1080,7 @@ func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1125,7 +1109,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{
|
||||
@@ -1182,7 +1166,7 @@ func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1211,7 +1195,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{
|
||||
@@ -1274,9 +1258,9 @@ func TestDeployWikiDoesNotRenameHeadinglessPageToPageIDFallback(t *testing.T) {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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{
|
||||
@@ -1336,7 +1320,7 @@ func TestDeployWikiRenamesBrokenHeadinglessFallbackTitleBackToPageIndexTitle(t *
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1361,7 +1345,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{
|
||||
@@ -1412,7 +1396,7 @@ func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t
|
||||
|
||||
createCalls := 0
|
||||
var updated string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1458,7 +1442,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")
|
||||
@@ -1525,7 +1509,7 @@ func TestDeployWikiMergesHTMLManagedAndManualRegions(t *testing.T) {
|
||||
}
|
||||
|
||||
var updated string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1549,7 +1533,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{
|
||||
@@ -1605,7 +1589,7 @@ func TestDeployWikiUpdateAcquiresWestgateWikiEditLock(t *testing.T) {
|
||||
|
||||
lockAcquired := false
|
||||
var updateToken string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1637,7 +1621,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{
|
||||
@@ -1684,7 +1668,7 @@ func TestDeployWikiCreateCollisionAdoptsExistingNodeBBPage(t *testing.T) {
|
||||
|
||||
createCalls := 0
|
||||
var updated string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1735,7 +1719,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")
|
||||
@@ -1787,7 +1771,7 @@ func TestDeployWikiCreateCollisionSearchesCanonicalTitleSegment(t *testing.T) {
|
||||
|
||||
createCalls := 0
|
||||
var updated string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -1844,7 +1828,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")
|
||||
@@ -1884,7 +1868,7 @@ func TestDeployWikiReportsAndArchivesStalePages(t *testing.T) {
|
||||
}
|
||||
|
||||
var archived string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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
|
||||
@@ -1905,7 +1889,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{
|
||||
@@ -1961,9 +1945,9 @@ func TestDeployWikiDryRunPlansTrackedStalePagePurge(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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{
|
||||
@@ -2000,9 +1984,9 @@ func TestDeployWikiPurgeRefusesStaleManifestEntryWithoutTopicID(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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{
|
||||
@@ -2033,12 +2017,15 @@ func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
var calls []string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if !answerWikiPagePurge(t, w, r, &calls) {
|
||||
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" {
|
||||
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{
|
||||
@@ -2054,8 +2041,8 @@ func TestDeployWikiPurgesTrackedStaleGeneratedTopic(t *testing.T) {
|
||||
if result.Stale != 1 || result.Purged != 1 {
|
||||
t.Fatalf("expected one stale purge, got %#v", result)
|
||||
}
|
||||
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 purgeCalls != 1 {
|
||||
t.Fatalf("expected one NodeBB topic purge call, got %d", purgeCalls)
|
||||
}
|
||||
if _, ok := loadDeployManifest(manifestPath).Pages["skills:retired"]; ok {
|
||||
t.Fatalf("expected purged stale manifest entry to be removed")
|
||||
@@ -2088,19 +2075,18 @@ func TestDeployWikiPurgesTrackedStaleTopicsBeforeCreatingReplacementPages(t *tes
|
||||
}
|
||||
|
||||
var calls []string
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if answerWikiPagePurge(t, w, r, &calls) {
|
||||
return
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pages": []any{}, "hasMore": false}})
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/99":
|
||||
respondWikiPost(w, 99, 11, generated)
|
||||
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 strings.Join(calls, ",") != "tombstone:7,hard-purge:7,create" {
|
||||
if len(calls) != 2 || calls[0] != "purge" {
|
||||
t.Fatalf("expected stale purge before create, got calls %#v", calls)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -2116,7 +2102,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{
|
||||
@@ -2134,7 +2120,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, ",") != "tombstone:7,hard-purge:7,create" {
|
||||
if strings.Join(calls, ",") != "purge,create" {
|
||||
t.Fatalf("expected purge before create, got %#v", calls)
|
||||
}
|
||||
}
|
||||
@@ -2164,12 +2150,8 @@ 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 := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if answerWikiPagePurge(t, w, r, &calls) {
|
||||
return
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(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")
|
||||
@@ -2182,11 +2164,17 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
"hasMore": false,
|
||||
},
|
||||
})
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/99":
|
||||
respondWikiPost(w, 99, 11, generated)
|
||||
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, ",") != resetCalls+",create" {
|
||||
if strings.Join(calls, ",") != "purge:7,purge:8,create" {
|
||||
t.Fatalf("expected namespace reset purges before create, got %#v", calls)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -2202,7 +2190,7 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2217,21 +2205,10 @@ func TestDeployWikiResetManagedNamespacesPurgesRemotePagesBeforeCreatingFreshMan
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions reset-managed-namespaces failed: %v", err)
|
||||
}
|
||||
if result.Created != 1 || result.Purged != 2 {
|
||||
if result.Created != 1 || result.Purged != 2 || result.Stale != 0 {
|
||||
t.Fatalf("expected one create and two namespace purges, got %#v", result)
|
||||
}
|
||||
// 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" {
|
||||
if strings.Join(calls, ",") != "purge:7,purge:8,create" {
|
||||
t.Fatalf("expected reset purges before create, got %#v", calls)
|
||||
}
|
||||
manifest := loadDeployManifest(manifestPath)
|
||||
@@ -2259,9 +2236,9 @@ func TestDeployWikiResetManagedNamespacesRequiresCreateForLocalPages(t *testing.
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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{
|
||||
@@ -2293,21 +2270,21 @@ func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T)
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
seen := map[string]int{}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
call, ok := wikiPagePurgeCall(t, r)
|
||||
if !ok {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
seen[call]++
|
||||
seen[r.URL.Path]++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// 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" {
|
||||
switch r.URL.Path {
|
||||
case "/api/v3/topics/7":
|
||||
http.Error(w, `{"status":{"message":"topic not found"}}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
@@ -2323,11 +2300,8 @@ func TestDeployWikiPurgeTreatsAlreadyMissingTrackedTopicAsSuccess(t *testing.T)
|
||||
if result.Stale != 2 || result.Purged != 2 {
|
||||
t.Fatalf("expected two stale purges, got %#v", result)
|
||||
}
|
||||
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)
|
||||
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)
|
||||
}
|
||||
manifest := loadDeployManifest(manifestPath)
|
||||
if _, ok := manifest.Pages["skills:retired"]; ok {
|
||||
@@ -2365,9 +2339,9 @@ func TestDeployWikiPurgeDoesNotTargetCurrentGeneratedPages(t *testing.T) {
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
server := httptest.NewServer(http.HandlerFunc(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{
|
||||
@@ -2411,415 +2385,3 @@ 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.
|
||||
// driftScenario is one managed page whose generated text changed since the last
|
||||
// deploy, so the deployer has to read the live post and rule on drift. The fake
|
||||
// NodeBB serves whatever it was last told to store, which is what a real deploy
|
||||
// reads back after a write.
|
||||
type driftScenario struct {
|
||||
sourceDir string
|
||||
manifestPath string
|
||||
endpoint string
|
||||
generated string
|
||||
stored string
|
||||
updateCalls int
|
||||
lastWritten string
|
||||
}
|
||||
|
||||
// newDriftScenario writes the local page and the manifest entry. remoteStored is
|
||||
// the body NodeBB hands back; hasSourceContent says whether the post carries the
|
||||
// sourceContent the wiki plugin stores.
|
||||
func newDriftScenario(t *testing.T, entry wikiDeployManifestPage, remoteStored string, hasSourceContent bool) (*driftScenario, *httptest.Server) {
|
||||
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)
|
||||
}
|
||||
generated := `<!-- sow-topdata-wiki:page=skills:athletics -->
|
||||
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||
<h1>Athletics</h1>
|
||||
<p>Generated athletics page</p>
|
||||
<!-- sow-topdata-wiki:managed:end -->
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "skills", "athletics.html"), []byte(generated), 0644); err != nil {
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
entry.TID, entry.PID, entry.CID = 7, 42, 3
|
||||
entry.SourceContentSynced = true
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||
Version: "nodebb-v1",
|
||||
Pages: map[string]wikiDeployManifestPage{"skills:athletics": entry},
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
scenario := &driftScenario{sourceDir: sourceDir, manifestPath: manifestPath, generated: generated, stored: remoteStored}
|
||||
server := newFakeNodeBB(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/posts/42":
|
||||
post := map[string]any{"pid": 42, "tid": 7, "content": scenario.stored}
|
||||
if hasSourceContent {
|
||||
post["sourceContent"] = scenario.stored
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": post})
|
||||
case r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/edit-lock":
|
||||
respondWikiEditLock(t, w, r, 7, "drift-lock")
|
||||
case r.Method == http.MethodPut && r.URL.Path == "/api/v3/posts/42":
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatalf("decode update request: %v", err)
|
||||
}
|
||||
scenario.updateCalls++
|
||||
scenario.lastWritten = req.Content
|
||||
// NodeBB owns the stored body from here on, and the read-back that
|
||||
// follows has to see it.
|
||||
scenario.stored = req.Content
|
||||
hasSourceContent = true
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"pid": 42, "tid": 7}})
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
})
|
||||
return scenario, server
|
||||
}
|
||||
|
||||
func (s *driftScenario) deploy(t *testing.T, force bool) (DeployResult, error) {
|
||||
t.Helper()
|
||||
return DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
SourceDir: s.sourceDir,
|
||||
Endpoint: s.endpoint,
|
||||
Token: "nodebb-token",
|
||||
ManifestPath: s.manifestPath,
|
||||
Force: force,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func TestDeployWikiDoesNotTreatNodeBBNormalizationAsDrift(t *testing.T) {
|
||||
// NodeBB stores its own copy of the page, so what it hands back never has
|
||||
// to be byte-equal to the text we rendered. Only a change since our last
|
||||
// write is drift.
|
||||
stored := `<!-- sow-topdata-wiki:page=skills:athletics -->
|
||||
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||
<h1>Athletics</h1><p>Old generated text</p>
|
||||
<!-- sow-topdata-wiki:managed:end -->
|
||||
`
|
||||
scenario, server := newDriftScenario(t, wikiDeployManifestPage{
|
||||
Hash: "sha256:whatever-we-rendered-last-time",
|
||||
RemoteHash: computeManagedHash(stored),
|
||||
}, stored, true)
|
||||
defer server.Close()
|
||||
scenario.endpoint = server.URL
|
||||
|
||||
result, err := scenario.deploy(t, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected normalized remote copy to deploy cleanly, got: %v", err)
|
||||
}
|
||||
if result.Drifted != 0 || result.Updated != 1 || scenario.updateCalls != 1 {
|
||||
t.Fatalf("expected a clean update, got %#v (updateCalls=%d)", result, scenario.updateCalls)
|
||||
}
|
||||
entry := loadDeployManifest(scenario.manifestPath).Pages["skills:athletics"]
|
||||
if entry.RemoteHash != computeManagedHash(scenario.lastWritten) {
|
||||
t.Fatalf("expected manifest to record the read-back remote hash, got %#v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWikiReportsEditedPageAsDriftedAndNamesIt(t *testing.T) {
|
||||
stored := `<!-- sow-topdata-wiki:page=skills:athletics -->
|
||||
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||
<h1>Athletics</h1>
|
||||
<p>A person rewrote this by hand.</p>
|
||||
<!-- sow-topdata-wiki:managed:end -->
|
||||
`
|
||||
scenario, server := newDriftScenario(t, wikiDeployManifestPage{
|
||||
Hash: "sha256:whatever-we-rendered-last-time",
|
||||
RemoteHash: "sha256:what-we-wrote-last-time",
|
||||
}, stored, true)
|
||||
defer server.Close()
|
||||
scenario.endpoint = server.URL
|
||||
|
||||
result, err := scenario.deploy(t, false)
|
||||
if err == nil {
|
||||
t.Fatalf("expected a hand-edited page to block the deploy, got %#v", result)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "skills:athletics") {
|
||||
t.Fatalf("expected the drift error to name the page, got: %v", err)
|
||||
}
|
||||
if result.Drifted != 1 || scenario.updateCalls != 0 {
|
||||
t.Fatalf("expected one drifted page and no write, got %#v (updateCalls=%d)", result, scenario.updateCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWikiForceOverwritesDriftedPageAndRerecordsRemoteHash(t *testing.T) {
|
||||
stored := `<!-- sow-topdata-wiki:page=skills:athletics -->
|
||||
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||
<h1>Athletics</h1>
|
||||
<p>A person rewrote this by hand.</p>
|
||||
<!-- sow-topdata-wiki:managed:end -->
|
||||
`
|
||||
scenario, server := newDriftScenario(t, wikiDeployManifestPage{
|
||||
Hash: "sha256:whatever-we-rendered-last-time",
|
||||
RemoteHash: "sha256:what-we-wrote-last-time",
|
||||
}, stored, true)
|
||||
defer server.Close()
|
||||
scenario.endpoint = server.URL
|
||||
|
||||
result, err := scenario.deploy(t, true)
|
||||
if err != nil {
|
||||
t.Fatalf("expected --force to overwrite the drifted page, got: %v", err)
|
||||
}
|
||||
if result.Drifted != 1 || result.Updated != 1 || scenario.updateCalls != 1 {
|
||||
t.Fatalf("expected the drifted page overwritten once, got %#v (updateCalls=%d)", result, scenario.updateCalls)
|
||||
}
|
||||
entry := loadDeployManifest(scenario.manifestPath).Pages["skills:athletics"]
|
||||
if entry.RemoteHash != computeManagedHash(scenario.lastWritten) {
|
||||
t.Fatalf("expected the forced write to re-record the remote hash, got %#v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWikiTreatsPostWithoutSourceContentAsUnsyncedNotDrifted(t *testing.T) {
|
||||
// A post written before the deployer stored sourceContent reads back as
|
||||
// NodeBB's rendered HTML, which can never match anything we wrote. That is
|
||||
// a page waiting for the sourceContent repair, not a hand edit.
|
||||
rendered := "<h1>Athletics</h1><p>Old generated text</p>"
|
||||
scenario, server := newDriftScenario(t, wikiDeployManifestPage{
|
||||
Hash: "sha256:whatever-we-rendered-last-time",
|
||||
}, rendered, false)
|
||||
defer server.Close()
|
||||
scenario.endpoint = server.URL
|
||||
|
||||
result, err := scenario.deploy(t, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected an unsynced page to deploy, got: %v", err)
|
||||
}
|
||||
if result.Drifted != 0 || result.Updated != 1 || scenario.updateCalls != 1 {
|
||||
t.Fatalf("expected one repair update and no drift, got %#v (updateCalls=%d)", result, scenario.updateCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWikiFallsBackToLocalHashForManifestWithoutRemoteHash(t *testing.T) {
|
||||
// Manifest written before remote_hash existed: the remote still matches the
|
||||
// recorded local hash, so nothing drifted, and this run records the remote
|
||||
// hash for the next one.
|
||||
stored := `<!-- sow-topdata-wiki:page=skills:athletics -->
|
||||
<!-- sow-topdata-wiki:managed:start hash="sha256:old" -->
|
||||
<h1>Athletics</h1>
|
||||
<p>Old generated text</p>
|
||||
<!-- sow-topdata-wiki:managed:end -->
|
||||
`
|
||||
scenario, server := newDriftScenario(t, wikiDeployManifestPage{
|
||||
Hash: computeManagedHash(stored),
|
||||
}, stored, true)
|
||||
defer server.Close()
|
||||
scenario.endpoint = server.URL
|
||||
|
||||
result, err := scenario.deploy(t, false)
|
||||
if err != nil {
|
||||
t.Fatalf("expected an old-format manifest to deploy, got: %v", err)
|
||||
}
|
||||
if result.Drifted != 0 || result.Updated != 1 {
|
||||
t.Fatalf("expected a clean update, got %#v", result)
|
||||
}
|
||||
entry := loadDeployManifest(scenario.manifestPath).Pages["skills:athletics"]
|
||||
if entry.RemoteHash == "" {
|
||||
t.Fatalf("expected the deploy to record a remote hash, got %#v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
// respondWikiPost answers a post read the way the wiki plugin does: the stored
|
||||
// source HTML in sourceContent, alongside the body NodeBB renders from it.
|
||||
func respondWikiPost(w http.ResponseWriter, pid, tid int, content string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{
|
||||
"pid": pid, "tid": tid, "content": content, "sourceContent": content,
|
||||
}})
|
||||
}
|
||||
|
||||
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