diff --git a/opendj-doc-generated-ref/pom.xml b/opendj-doc-generated-ref/pom.xml index 4bcfdf20e4..93d2248bdb 100644 --- a/opendj-doc-generated-ref/pom.xml +++ b/opendj-doc-generated-ref/pom.xml @@ -34,8 +34,24 @@ UTF-8 ${project.version} ${project.version} + + 2.5.11 + + + org.asciidoctor + asciidoctorj + ${asciidoctorj.version} + test + + + org.openidentityplatform.commons + build-tools + test + + + ${project.groupId}.${project.artifactId} @@ -356,6 +372,34 @@ + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-extension-tests + test-compile + + testCompile + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + test-extensions + test + + test + + + + @@ -603,7 +647,66 @@ ${project.build.directory}/asciidoc/man-pages + + + check-attribute-references + verify + + process-asciidoc + + + + ${project.basedir}/src/main/resources/asciidoc/extensions/nested-open-block.rb + ${project.basedir}/src/main/resources/asciidoc/extensions/literal-attribute-references.rb + + html5 + ${project.build.directory}/asciidoc/source + ${project.build.directory}/asciidoc/attribute-check + true + true + true + + warn + ${project.build.directory}/asciidoc/source + + + + WARN + attribute + + + + + + + org.asciidoctor + asciidoctorj + ${asciidoctorj.version} + + diff --git a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-monitoring.adoc b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-monitoring.adoc index 435b5e97d7..11734a1d56 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-monitoring.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-monitoring.adoc @@ -79,7 +79,7 @@ SNMP is not enabled by default. SNMP-based monitoring depends on OpenDMK, which To run the OpenDMK installer, use the self-extracting .jar: -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ java -jar ~/Downloads/opendmk-1.0-b02-*.jar $ cd ~/Downloads/ @@ -150,7 +150,7 @@ $ dsconfig \ ---- Use a command such as `snmpwalk` to check that the SNMP listen port works: -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ snmpwalk -v 2c -c OpenDJ@OpenDJ localhost:11161 SNMPv2-SMI::mib-2.66.1.1.1.1 = STRING: "OpenDJ {opendj-version}..." @@ -248,7 +248,7 @@ OpenDJ comes with two commands for monitoring server processes and tasks. The `s The `status` command takes administrative credentials to read the configuration, as does the control panel: -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ status --bindDN "cn=Directory Manager" --bindPassword password diff --git a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-pwd-policy.adoc b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-pwd-policy.adoc index 20df95971f..0c7e404f53 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-pwd-policy.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-pwd-policy.adoc @@ -12,7 +12,7 @@ information: "Portions copyright [year] [name of copyright owner]". Copyright 2017 ForgeRock AS. - Portions Copyright 2024 3A Systems LLC. + Portions Copyright 2024-2026 3A Systems LLC. //// :figure-caption!: @@ -1009,7 +1009,7 @@ $ ldappasswordmodify \ --authzID "u:bjensen" \ --newPassword '!ABcd$%^' ---- -In the preceding example, the character set of ASCII punctuation, ``!\"#\$%&\'\(\)*+,-./:\;\\<=\>?@[\\]^_\`{\|}~``, is hard to read because of all the escape characters. In practice it can be easier to enter sequences like that by using `dsconfig` in interactive mode, and letting it do the escaping for you. You can also use the `--commandFilePath {path}` option to save the result of your interactive session to a file for use in scripts later. +In the preceding example, the character set of ASCII punctuation, ``!\"#\$%&\'\(\)*+,-./:\;\\<=\>?@[\\]^_\`{\|}~``, is hard to read because of all the escape characters. In practice it can be easier to enter sequences like that by using `dsconfig` in interactive mode, and letting it do the escaping for you. You can also use the `--commandFilePath \{path}` option to save the result of your interactive session to a file for use in scripts later. An attempt to set an invalid password fails as shown in the following example: diff --git a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-troubleshooting.adoc b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-troubleshooting.adoc index 81342776ea..c63e47c646 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-troubleshooting.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-troubleshooting.adoc @@ -808,7 +808,7 @@ OpenDJ maintains historical information about changes in order to bring replicas When you cannot resolve a problem yourself, and want to ask for help, clearly identify the problem and how you reproduce it, and also the version of OpenDJ you use to reproduce the problem. The version includes both a version number and also a build time stamp: -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ dsconfig --version OpenDJ {opendj-version} diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc index 106f82998a..f5e9f1e275 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-install.adoc @@ -383,7 +383,7 @@ If you do not start the server during installation, you can use the `/path/to/op . Run the `status` command, described in xref:../reference/admin-tools-ref.adoc#status-1[status(1)] in the __Reference__, to make sure your OpenDJ server is working as expected as shown in the following example: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ /path/to/opendj/bin/status @@ -437,7 +437,7 @@ On Debian and related Linux distributions such as Ubuntu, you can install OpenDJ . Install the OpenDJ directory server package. Use `apt-get install ./.deb` (rather than `dpkg -i`) so the required Java runtime dependency (`default-jre-headless`) is resolved and installed automatically: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ sudo apt-get install ./opendj_{opendj-version}-1_all.deb ---- @@ -463,7 +463,7 @@ $ sudo systemctl start opendj . (Optional) Check OpenDJ directory server status: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ systemctl is-active opendj active @@ -525,7 +525,7 @@ Password: . Install the OpenDJ directory server package. Use `dnf install ./.rpm` (rather than `rpm -i`) so the required Java runtime dependency (`java-headless >= 11`) is resolved and installed automatically: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- # dnf install ./opendj-{opendj-version}-1.noarch.rpm Post Install - initial install @@ -552,7 +552,7 @@ To see basic server configuration status and configuration you can launch . (Optional) Check OpenDJ directory server status: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- # systemctl is-active opendj active @@ -623,7 +623,7 @@ The package is not code-signed, so Windows SmartScreen or User Account Control m * Silent: run the following command (optionally set the installation directory with the `OPENDJ` property): + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\opendj" ---- @@ -701,7 +701,7 @@ If you have multiple servers to install, consider scripting creation of the prop . Prepare an installation script: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ cat /net/install/dj/1/setup.sh #!/bin/sh @@ -756,7 +756,7 @@ END_OF_COMMAND_INPUT . Run your installation script: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ /net/install/dj/1/setup.sh Archive: /net/install/dj/opendj-{opendj-version}.zip diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc index af448ffb9b..1cff64e268 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc @@ -176,7 +176,7 @@ C:\path\to\opendj\bat> windows-service.bat --disableService . Uninstall the package, either through __Settings > Apps__ (or __Control Panel > Programs and Features__) by selecting OpenDJ and choosing Uninstall, or from the command-line: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- C:\> msiexec /x opendj-{opendj-version}.msi /quiet ---- diff --git a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc index 7cca685d73..180e9b993d 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-upgrade.adoc @@ -153,7 +153,7 @@ C:\path\to\opendj\bat> windows-service.bat --enableService ==== The following example upgrades an OpenDJ 2.6.3 directory server, backing up the current server directory in case the upgrade process fails. In this example, the server properties are updated to use Java 11, and the Local DB backend is migrated to a JE backend: -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ cd /path/to/ $ sed -e "s/default.java-home=.*/default.java-home=\/path\/to\/jdk11/" \ @@ -268,7 +268,7 @@ A server started with `start-ds.bat` rather than as a service is refused in the . Install the newer package (GUI or silent). The installer detects the existing installation — the location recorded in the registry by a previous x64 package, or the default directory of the older 32-bit package (`C:\Program Files (x86)\OpenDJ`) — and installs into the same directory, so your configured instance data (`config`, `db`, `logs`) is kept and only the program files are replaced. If the older server was installed in a custom directory the installer cannot detect, select that directory in the wizard or pass it explicitly on the command line: rather than installing a fresh server into the default directory while emptying the old one, the installer refuses to continue whenever nothing has recorded where the old server lives and the directory it is about to install into holds no OpenDJ server -- which also catches a mistyped directory name. That refusal also covers an old server that really is installed in `C:\Program Files\OpenDJ`, because the 32-bit packages recorded no location at all — and that one case the wizard cannot resolve: choosing the default directory in the wizard leaves the installer with the same values it would have had if you had chosen nothing, so pass `OPENDJ` on the command line instead — it can be given with or without `/quiet`, so a wizard installation takes it just as a silent one does. The installer further refuses to install into a directory other than the one it detected, unless the directory you name holds an OpenDJ server itself (see the note below): it replaces an installation in place and cannot move one, so uninstall the existing server first if you want it somewhere else. + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- C:\> msiexec /i opendj-{opendj-version}.msi /quiet OPENDJ="C:\path\to\opendj" ---- diff --git a/opendj-doc-generated-ref/src/main/asciidoc/man-pages/man-windows-service.adoc b/opendj-doc-generated-ref/src/main/asciidoc/man-pages/man-windows-service.adoc index 2a74099801..15645548dc 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/man-pages/man-windows-service.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/man-pages/man-windows-service.adoc @@ -13,7 +13,7 @@ information: "Portions Copyright [year] [name of copyright owner]". Copyright 2015-2016 ForgeRock AS. - Portions Copyright 2024 3A Systems LLC. + Portions Copyright 2024-2026 3A Systems LLC. //// @@ -28,7 +28,7 @@ windows-service - register OpenDJ as a Windows Service == Synopsis -`windows-service` {options} +`windows-service` \{options} == Description This utility can be used to run OpenDJ directory server as a Windows Service. diff --git a/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-writing-plugins.adoc b/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-writing-plugins.adoc index 0e40bd462d..59498dcd35 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-writing-plugins.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-writing-plugins.adoc @@ -170,7 +170,7 @@ $ mvn install . Install the example plugin in OpenDJ directory server: + -[source, console, subs="attributes"] +[source, console, subs="+attributes"] ---- $ cd /path/to/opendj diff --git a/opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb b/opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb new file mode 100644 index 0000000000..5bad0aa468 --- /dev/null +++ b/opendj-doc-generated-ref/src/main/resources/asciidoc/extensions/literal-attribute-references.rb @@ -0,0 +1,88 @@ +# The contents of this file are subject to the terms of the Common Development and +# Distribution License (the License). You may not use this file except in compliance with the +# License. +# +# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the +# specific language governing permission and limitations under the License. +# +# When distributing Covered Software, include this CDDL Header Notice in each file and include +# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL +# Header, with the fields enclosed by brackets [] replaced by your own identifying +# information: "Portions copyright [year] [name of copyright owner]". +# +# Copyright 2026 3A Systems, LLC. + +# Warns about an attribute reference that a verbatim block publishes as literal text. +# +# A listing, literal or passthrough block replaces {name} only when its subs include +# attributes (subs="+attributes"). Without them the braces reach the page as they are, +# and Asciidoctor says nothing: `unzip opendj-{opendj-version}.zip` was published that +# way. A literal table cell (l|) never substitutes attributes and takes no subs. A +# reference to an attribute that is not defined at all, outside such a block, is +# Asciidoctor's own warning once attribute-missing is set to warn. +# +# A reference is reported when its name is an attribute at that point of the document, +# an intrinsic one such as {nbsp}, or one that an attribute entry anywhere under the +# directory named by the literal-attribute-sources attribute sets. The last catches a +# page that neither defines the attribute nor substitutes it. Braces around any other +# name are meant literally - {SSHA} password values, {cn} in a MakeLDIF template - and +# are left alone. Without attribute subs a backslash does not escape the reference, so +# \{name} is published with its backslash and is reported too. +require 'set' + +class LiteralAttributeReferences < Asciidoctor::Extensions::TreeProcessor + include Asciidoctor::Logging + + ReferenceRx = /(\\)?\{(\w[\w-]*)\}/ + EntryRx = /^:(\w[\w-]*):/ + + @names_by_dir = {} + + # The names that an attribute entry sets in some .adoc file under dir. The directory is + # the base of the glob, not a part of the pattern, so braces in its path match as such. + def self.names_in dir + @names_by_dir[dir] ||= Dir.glob('**/*.adoc', base: dir).each_with_object(Set.new) do |path, names| + File.foreach((File.join dir, path), encoding: 'UTF-8') {|line| names << $1.downcase if EntryRx =~ line } + end + end + + def process document + dir = document.attr 'literal-attribute-sources' + names = dir ? (LiteralAttributeReferences.names_in dir) : Set.new + # The parser has already reset the document attributes to the header, so replay the + # entries of the body in document order, as the converter does, and reset them again + # for the converter afterwards. An AsciiDoc table cell is a document of its own. + documents = [] + document.find_by traverse_documents: true do |block| + documents << block if block.context == :document + block.document.playback_attributes block.attributes + if Asciidoctor::Table::Cell === block + # The text of a literal cell only escapes special characters, so its braces stay. + check block, block.text, names, 'literal table cell', 'use an a| cell with a listing that has subs="+attributes"' if block.content_model == :verbatim + elsif Asciidoctor::Block === block && + (block.content_model == :verbatim || block.content_model == :raw) && !(block.subs.include? :attributes) + # subs="attributes" would replace the default subs of the block, so the advice adds to them. + check block, (block.lines.join Asciidoctor::LF), names, %(#{block.context} block), 'add subs="+attributes"' + end + false + end + documents.each(&:restore_attributes) + nil + end + + def check block, text, names, what, advice + attributes = block.document.attributes + text.scan(ReferenceRx) do |escaped, name| + key = name.downcase + next unless (attributes.key? key) || (names.include? key) || (Asciidoctor::INTRINSIC_ATTRIBUTES.key? key) + message = escaped ? + %(\\{#{name}} is published with its backslash: the #{what} does not substitute attributes, #{advice}) : + %(attribute {#{name}} is published as literal text: the #{what} does not substitute attributes, #{advice}) + logger.warn message_with_context message, source_location: block.source_location + end + end +end + +Asciidoctor::Extensions.register do + tree_processor LiteralAttributeReferences +end diff --git a/opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/LiteralAttributeReferencesTest.java b/opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/LiteralAttributeReferencesTest.java new file mode 100644 index 0000000000..2be5279f88 --- /dev/null +++ b/opendj-doc-generated-ref/src/test/java/org/openidentityplatform/opendj/doc/LiteralAttributeReferencesTest.java @@ -0,0 +1,172 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package org.openidentityplatform.opendj.doc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; + +import org.asciidoctor.Asciidoctor; +import org.asciidoctor.Options; +import org.asciidoctor.SafeMode; +import org.asciidoctor.log.LogRecord; +import org.asciidoctor.log.Severity; +import org.forgerock.testng.ForgeRockTestCase; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * Renders pages with the configuration of the check-attribute-references execution of this module's + * pom - its extensions, its attributes and its failIf - and checks which of them would fail the build. + * Reading the configuration from the pom keeps the execution itself under test, not only + * literal-attribute-references.rb. + */ +@Test +public class LiteralAttributeReferencesTest extends ForgeRockTestCase { + private static final String EXECUTION = "check-attribute-references"; + + private final List records = new ArrayList<>(); + private final Map attributes = new LinkedHashMap<>(); + private Asciidoctor asciidoctor; + private String backend; + private boolean sourcemap; + private Severity failSeverity; + private String failText; + + @BeforeClass + public void readExecution() throws Exception { + final File module = new File(System.getProperty("basedir", ".")); + // The pre-processed sources sit under the build directory. Braces and brackets in its path + // must not turn it into a glob pattern. + final Path buildDirectory = Files.createTempDirectory("attribute-check-{1}[1]-"); + final Path sources = Files.createDirectories(buildDirectory.resolve("asciidoc/source/other-guide")); + Files.write(sources.resolve("chap-other.adoc"), ":elsewhere: 1\n".getBytes(StandardCharsets.UTF_8)); + + final XPath xpath = XPathFactory.newInstance().newXPath(); + final Element configuration = (Element) xpath.evaluate( + "//execution[id='" + EXECUTION + "']/configuration", + DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(new File(module, "pom.xml")), + XPathConstants.NODE); + assertThat(configuration).as("configuration of the " + EXECUTION + " execution").isNotNull(); + + asciidoctor = Asciidoctor.Factory.create(); + asciidoctor.registerLogHandler(records::add); + final NodeList requires = (NodeList) xpath.evaluate("requires/require", configuration, XPathConstants.NODESET); + for (int i = 0; i < requires.getLength(); i++) { + asciidoctor.requireLibrary(requires.item(i).getTextContent().trim() + .replace("${project.basedir}", module.getAbsolutePath())); + } + final NodeList entries = (NodeList) xpath.evaluate("attributes/*", configuration, XPathConstants.NODESET); + for (int i = 0; i < entries.getLength(); i++) { + final Node entry = entries.item(i); + attributes.put(entry.getNodeName(), entry.getTextContent().trim() + .replace("${project.build.directory}", buildDirectory.toString())); + } + backend = xpath.evaluate("backend", configuration); + sourcemap = Boolean.parseBoolean(xpath.evaluate("sourcemap", configuration)); + failSeverity = Severity.valueOf(xpath.evaluate("logHandler/failIf/severity", configuration)); + failText = xpath.evaluate("logHandler/failIf/containsText", configuration); + } + + @AfterClass(alwaysRun = true) + public void shutdown() { + if (asciidoctor != null) { + asciidoctor.shutdown(); + } + } + + /** Returns the messages that make the execution fail the build on this page. */ + private List failures(final String page) { + records.clear(); + asciidoctor.convert(page, Options.builder() + .safe(SafeMode.UNSAFE) + .backend(backend) + .sourcemap(sourcemap) + .attributes(new LinkedHashMap<>(attributes)) + .toFile(false) + .build()); + return records.stream() + .filter(r -> r.getSeverity().ordinal() >= failSeverity.ordinal() && r.getMessage().contains(failText)) + .map(LogRecord::getMessage) + .collect(Collectors.toList()); + } + + @DataProvider + public Object[][] failingPages() { + return new Object[][] { + { ":v: 1\n\n----\nunzip x-{v}.zip\n----\n", + "attribute {v} is published as literal text: the listing block does not substitute attributes, " + + "add subs=\"+attributes\"" }, + { ":v: 1\n\n literal x-{v}\n", "attribute {v} is published as literal text: the literal block" }, + { ":v: 1\n\n++++\n

{v}

\n++++\n", "attribute {v} is published as literal text: the pass block" }, + { ":v: 1\n\n|===\nl|cell x-{v}\n|===\n", + "attribute {v} is published as literal text: the literal table cell does not substitute attributes, " + + "use an a| cell with a listing that has subs=\"+attributes\"" }, + { ":v: 1\n\n[cols=\"1l\"]\n|===\n|cell x-{v}\n|===\n", "the literal table cell" }, + { ":v: 1\n\n|===\na|\n----\nx-{v}\n----\n|===\n", "attribute {v} is published as literal text: the listing" }, + { ":v: 1\n\n----\nunzip x-\\{v}.zip\n----\n", "\\{v} is published with its backslash: the listing block" }, + { "----\nPATH{nbsp}x\n----\n", "attribute {nbsp} is published as literal text" }, + // A body entry counts from where it stands, not only a header one. + { "= Title\n\n== Section\n:late: 1\n\n----\nx-{late}\n----\n", "attribute {late} is published as literal text" }, + // A page that neither defines nor substitutes an attribute another page defines. + { "----\nx-{elsewhere}\n----\n", "attribute {elsewhere} is published as literal text" }, + { "tool {undefinedthing}\n", "undefinedthing" }, + }; + } + + @Test(dataProvider = "failingPages") + public void pageFailsTheBuild(final String page, final String message) { + assertThat(failures(page)).as(page).anySatisfy(failure -> assertThat(failure).contains(message)); + } + + @DataProvider + public Object[][] passingPages() { + return new Object[][] { + { ":v: 1\n\n[subs=\"+attributes\"]\n----\nunzip x-{v}.zip\n----\n" }, + { ":v: 1\n\n[subs=\"+attributes\"]\n----\nunzip x-\\{v}.zip\n----\n" }, + { ":v: 1\n\nunzip x-{v}.zip and \\{v}\n" }, + { ":v: 1\n\n|===\na|\n[subs=\"+attributes\"]\n----\nx-{v}\n----\n|===\n" }, + // Braces that are no attribute anywhere are meant literally. + { "----\nuserPassword: {SSHA}abc\ncn: {cn}\n----\n" }, + { "|===\nl|{SSHA}abc\n|===\n" }, + // An attribute unset in the body is no longer one where the listing stands. + { "= Title\n:v: 1\n\n== Section\n:v!:\n\n----\nx-{v}\n----\n" }, + }; + } + + @Test(dataProvider = "passingPages") + public void pagePassesTheBuild(final String page) { + assertThat(failures(page)).as(page).isEmpty(); + } +}