| /* | |
| * Copyright (c) 2021, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test | |
| * @summary Negative tests for the jwebserver command-line tool | |
| * @library /test/lib | |
| * @modules jdk.httpserver | |
| * @run testng/othervm CommandLineNegativeTest | |
| */ | |
| import java.io.IOException; | |
| import java.net.InetAddress; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import jdk.test.lib.Platform; | |
| import jdk.test.lib.process.OutputAnalyzer; | |
| import jdk.test.lib.process.ProcessTools; | |
| import jdk.test.lib.util.FileUtils; | |
| import org.testng.SkipException; | |
| import org.testng.annotations.AfterTest; | |
| import org.testng.annotations.BeforeTest; | |
| import org.testng.annotations.DataProvider; | |
| import org.testng.annotations.Test; | |
| import static java.lang.System.out; | |
| import static org.testng.Assert.assertFalse; | |
| public class CommandLineNegativeTest { | |
| static final Path JAVA_HOME = Path.of(System.getProperty("java.home")); | |
| static final String JWEBSERVER = getJwebserver(JAVA_HOME); | |
| static final String LOCALE_OPT = "-J-Duser.language=en -J-Duser.country=US"; | |
| static final Path CWD = Path.of(".").toAbsolutePath().normalize(); | |
| static final Path TEST_DIR = CWD.resolve("CommandLineNegativeTest"); | |
| static final Path TEST_FILE = TEST_DIR.resolve("file.txt"); | |
| static final String LOOPBACK_ADDR = InetAddress.getLoopbackAddress().getHostAddress(); | |
| @BeforeTest | |
| public void setup() throws IOException { | |
| if (Files.exists(TEST_DIR)) { | |
| FileUtils.deleteFileTreeWithRetry(TEST_DIR); | |
| } | |
| Files.createDirectories(TEST_DIR); | |
| Files.createFile(TEST_FILE); | |
| } | |
| @DataProvider | |
| public Object[][] unknownOption() { | |
| return new Object[][] { | |
| {"--unknownOption"}, | |
| {"null"} | |
| }; | |
| } | |
| @Test(dataProvider = "unknownOption") | |
| public void testBadOption(String opt) throws Throwable { | |
| out.println("\n--- testUnknownOption, opt=\"%s\" ".formatted(opt)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: unknown option: " + opt); | |
| } | |
| @DataProvider | |
| public Object[][] tooManyOptionArgs() { | |
| return new Object[][] { | |
| {"-b", "localhost"}, | |
| {"-d", "/some/path"}, | |
| {"-o", "none"}, | |
| {"-p", "0"}, | |
| {"--bind-address", "localhost"}, | |
| {"--directory", "/some/path"}, | |
| {"--output", "none"}, | |
| {"--port", "0"} | |
| // doesn't fail for -h option | |
| }; | |
| } | |
| @Test(dataProvider = "tooManyOptionArgs") | |
| public void testTooManyOptionArgs(String opt, String arg) throws Throwable { | |
| out.println("\n--- testTooManyOptionArgs, opt=\"%s\" ".formatted(opt)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt, arg, arg) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: unknown option: " + arg); | |
| } | |
| @DataProvider | |
| public Object[][] noArg() { | |
| return new Object[][] { | |
| {"-b", """ | |
| -b, --bind-address - Address to bind to. Default: %s (loopback). | |
| For all interfaces use "-b 0.0.0.0" or "-b ::".""".formatted(LOOPBACK_ADDR)}, | |
| {"-d", "-d, --directory - Directory to serve. Default: current directory."}, | |
| {"-o", "-o, --output - Output format. none|info|verbose. Default: info."}, | |
| {"-p", "-p, --port - Port to listen on. Default: 8000."}, | |
| {"--bind-address", """ | |
| -b, --bind-address - Address to bind to. Default: %s (loopback). | |
| For all interfaces use "-b 0.0.0.0" or "-b ::".""".formatted(LOOPBACK_ADDR)}, | |
| {"--directory", "-d, --directory - Directory to serve. Default: current directory."}, | |
| {"--output", "-o, --output - Output format. none|info|verbose. Default: info."}, | |
| {"--port", "-p, --port - Port to listen on. Default: 8000."} | |
| // doesn't fail for -h option | |
| }; | |
| } | |
| @Test(dataProvider = "noArg") | |
| public void testNoArg(String opt, String msg) throws Throwable { | |
| out.println("\n--- testNoArg, opt=\"%s\" ".formatted(opt)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: no value given for " + opt) | |
| .shouldContain(msg); | |
| } | |
| @DataProvider | |
| public Object[][] invalidValue() { | |
| return new Object[][] { | |
| {"-b", "[127.0.0.1]"}, | |
| {"-b", "badhost"}, | |
| {"--bind-address", "192.168.1.220..."}, | |
| {"-o", "bad-output-level"}, | |
| {"--output", "bad-output-level"}, | |
| {"-p", "+-"}, | |
| {"--port", "+-"} | |
| }; | |
| } | |
| @Test(dataProvider = "invalidValue") | |
| public void testInvalidValue(String opt, String val) throws Throwable { | |
| out.println("\n--- testInvalidValue, opt=\"%s\" ".formatted(opt)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt, val) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: invalid value given for " + opt + ": " + val); | |
| } | |
| @DataProvider | |
| public Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; } | |
| @Test(dataProvider = "portOptions") | |
| public void testPortOutOfRange(String opt) throws Throwable { | |
| out.println("\n--- testPortOutOfRange, opt=\"%s\" ".formatted(opt)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt, "65536") // range 0 to 65535 | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: server config failed: " + "port out of range:65536"); | |
| } | |
| @DataProvider | |
| public Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; } | |
| @Test(dataProvider = "directoryOptions") | |
| public void testRootNotADirectory(String opt) throws Throwable { | |
| out.println("\n--- testRootNotADirectory, opt=\"%s\" ".formatted(opt)); | |
| var file = TEST_FILE.toString(); | |
| assertFalse(Files.isDirectory(TEST_FILE)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt, file) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: server config failed: " + "Path is not a directory: " + file); | |
| } | |
| @Test(dataProvider = "directoryOptions") | |
| public void testRootDoesNotExist(String opt) throws Throwable { | |
| out.println("\n--- testRootDoesNotExist, opt=\"%s\" ".formatted(opt)); | |
| Path root = TEST_DIR.resolve("not/existent/dir"); | |
| assertFalse(Files.exists(root)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt, root.toString()) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: server config failed: " + "Path does not exist: " + root.toString()); | |
| } | |
| @Test(dataProvider = "directoryOptions") | |
| public void testRootNotReadable(String opt) throws Throwable { | |
| out.println("\n--- testRootNotReadable, opt=\"%s\" ".formatted(opt)); | |
| if (Platform.isWindows()) { | |
| // Not applicable to Windows. Reason: cannot revoke an owner's read | |
| // access to a directory that was created by that owner | |
| throw new SkipException("cannot run on Windows"); | |
| } | |
| Path root = Files.createDirectories(TEST_DIR.resolve("not/readable/dir")); | |
| try { | |
| root.toFile().setReadable(false, false); | |
| assertFalse(Files.isReadable(root)); | |
| simpleserver(JWEBSERVER, LOCALE_OPT, opt, root.toString()) | |
| .shouldNotHaveExitValue(0) | |
| .shouldContain("Error: server config failed: " + "Path is not readable: " + root.toString()); | |
| } finally { | |
| root.toFile().setReadable(true, false); | |
| } | |
| } | |
| @AfterTest | |
| public void teardown() throws IOException { | |
| if (Files.exists(TEST_DIR)) { | |
| FileUtils.deleteFileTreeWithRetry(TEST_DIR); | |
| } | |
| } | |
| // --- infra --- | |
| static String getJwebserver(Path image) { | |
| boolean isWindows = System.getProperty("os.name").startsWith("Windows"); | |
| Path jwebserver = image.resolve("bin").resolve(isWindows ? "jwebserver.exe" : "jwebserver"); | |
| if (Files.notExists(jwebserver)) | |
| throw new RuntimeException(jwebserver + " not found"); | |
| return jwebserver.toAbsolutePath().toString(); | |
| } | |
| static OutputAnalyzer simpleserver(String... args) throws Throwable { | |
| var pb = new ProcessBuilder(args) | |
| .directory(TEST_DIR.toFile()); | |
| var outputAnalyser = ProcessTools.executeCommand(pb) | |
| .outputTo(System.out) | |
| .errorTo(System.out); | |
| return outputAnalyser; | |
| } | |
| } |