1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.cli;
7   
8   import static org.assertj.core.api.Assertions.assertThat;
9   import static org.junit.jupiter.api.Assertions.assertAll;
10  import static org.junit.jupiter.api.Assertions.assertEquals;
11  import static org.junit.jupiter.api.Assertions.assertNull;
12  
13  import gov.nist.secauto.metaschema.cli.processor.ExitCode;
14  import gov.nist.secauto.metaschema.cli.processor.ExitStatus;
15  
16  import org.junit.jupiter.api.Test;
17  import org.junit.jupiter.api.parallel.Execution;
18  import org.junit.jupiter.api.parallel.ExecutionMode;
19  import org.junit.jupiter.params.ParameterizedTest;
20  import org.junit.jupiter.params.provider.Arguments;
21  import org.junit.jupiter.params.provider.MethodSource;
22  
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.stream.Stream;
26  
27  import edu.umd.cs.findbugs.annotations.NonNull;
28  import nl.altindag.log.LogCaptor;
29  
30  /**
31   * Unit test for simple CLI.
32   */
33  @Execution(value = ExecutionMode.SAME_THREAD, reason = "Log capturing needs to be single threaded")
34  public class CLITest {
35    private static final ExitCode NO_EXCEPTION_CLASS = null;
36  
37    void evaluateResult(@NonNull ExitStatus status, @NonNull ExitCode expectedCode) {
38      status.generateMessage(true);
39      assertAll(() -> assertEquals(expectedCode, status.getExitCode(), "exit code mismatch"),
40          () -> assertNull(status.getThrowable(), "expected null Throwable"));
41    }
42  
43    void evaluateResult(@NonNull ExitStatus status, @NonNull ExitCode expectedCode,
44        @NonNull Class<? extends Throwable> thrownClass) {
45      Throwable thrown = status.getThrowable();
46      assertAll(
47          () -> assertEquals(expectedCode, status.getExitCode(), "exit code mismatch"),
48          () -> assertEquals(thrownClass, thrown == null ? null : thrown.getClass(), "expected Throwable mismatch"));
49    }
50  
51    private static Stream<Arguments> providesValues() {
52      @SuppressWarnings("serial")
53      List<Arguments> values = new LinkedList<>() {
54        {
55          add(Arguments.of(new String[] {}, ExitCode.INVALID_COMMAND,
56              NO_EXCEPTION_CLASS));
57          add(Arguments.of(new String[] { "-h" }, ExitCode.OK, NO_EXCEPTION_CLASS));
58          add(Arguments.of(new String[] { "generate-schema", "--help" }, ExitCode.OK,
59              NO_EXCEPTION_CLASS));
60          add(Arguments.of(new String[] { "generate-diagram", "--help" }, ExitCode.OK,
61              NO_EXCEPTION_CLASS));
62          add(Arguments.of(new String[] { "validate", "--help" }, ExitCode.OK,
63              NO_EXCEPTION_CLASS));
64          add(Arguments.of(new String[] { "validate-content", "--help" }, ExitCode.OK,
65              NO_EXCEPTION_CLASS));
66          add(Arguments.of(new String[] { "convert", "--help" }, ExitCode.OK,
67              NO_EXCEPTION_CLASS));
68          add(Arguments.of(new String[] { "metapath", "list-functions", "--help" }, ExitCode.OK,
69              NO_EXCEPTION_CLASS));
70          add(Arguments.of(new String[] { "metapath", "eval", "--help" }, ExitCode.OK,
71              NO_EXCEPTION_CLASS));
72          add(Arguments.of(
73              new String[] { "validate",
74                  "../databind/src/test/resources/metaschema/fields_with_flags/metaschema.xml"
75              },
76              ExitCode.OK, NO_EXCEPTION_CLASS));
77          add(Arguments.of(
78              new String[] { "generate-schema", "--overwrite", "--as",
79                  "JSON",
80                  "../databind/src/test/resources/metaschema/fields_with_flags/metaschema.xml",
81                  "target/schema-test.json" },
82              ExitCode.OK, NO_EXCEPTION_CLASS));
83          add(Arguments.of(
84              new String[] { "validate-content", "--as=xml",
85                  "-m=../databind/src/test/resources/metaschema/bad_index-has-key/metaschema.xml",
86                  "../databind/src/test/resources/metaschema/bad_index-has-key/example.xml",
87                  "--show-stack-trace" },
88              ExitCode.FAIL, NO_EXCEPTION_CLASS));
89          add(Arguments.of(
90              new String[] { "validate-content", "--as=json",
91                  "-m=../databind/src/test/resources/metaschema/bad_index-has-key/metaschema.xml",
92                  "../databind/src/test/resources/metaschema/bad_index-has-key/example.json", "--show-stack-trace" },
93              ExitCode.FAIL, NO_EXCEPTION_CLASS));
94          add(Arguments.of(
95              new String[] { "validate",
96                  "../databind/src/test/resources/metaschema/simple/metaschema.xml",
97                  "--show-stack-trace" },
98              ExitCode.OK, NO_EXCEPTION_CLASS));
99          add(Arguments.of(
100             new String[] { "generate-schema",
101                 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
102                 "--as", "xml",
103             },
104             ExitCode.OK, NO_EXCEPTION_CLASS));
105         add(Arguments.of(
106             new String[] { "generate-schema",
107                 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
108                 "--as", "json",
109             },
110             ExitCode.OK, NO_EXCEPTION_CLASS));
111         add(Arguments.of(
112             new String[] { "generate-diagram",
113                 "../databind/src/test/resources/metaschema/simple/metaschema.xml"
114             },
115             ExitCode.OK, NO_EXCEPTION_CLASS));
116         add(Arguments.of(
117             new String[] { "validate-content",
118                 "-m",
119                 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
120                 "../databind/src/test/resources/metaschema/simple/example.json",
121                 "--as=json"
122             },
123             ExitCode.OK, NO_EXCEPTION_CLASS));
124         add(Arguments.of(
125             new String[] { "validate-content",
126                 "-m",
127                 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
128                 "../databind/src/test/resources/metaschema/simple/example.xml",
129                 "--as=xml"
130             },
131             ExitCode.OK, NO_EXCEPTION_CLASS));
132         add(Arguments.of(
133             new String[] { "validate-content",
134                 "-m",
135                 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
136                 "https://bad.domain.example.net/example.xml",
137                 "--as=xml"
138             },
139             ExitCode.IO_ERROR, java.net.UnknownHostException.class));
140         add(Arguments.of(
141             new String[] { "validate-content",
142                 "-m",
143                 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
144                 "https://github.com/no-example.xml",
145                 "--as=xml"
146             },
147             ExitCode.IO_ERROR, java.io.FileNotFoundException.class));
148         add(Arguments.of(
149             new String[] { "validate-content",
150                 "-m",
151                 "src/test/resources/content/schema-validation-module.xml",
152                 "src/test/resources/content/schema-validation-module-missing-required.xml",
153                 "--as=xml"
154             },
155             // fail due to schema validation issue
156             ExitCode.FAIL, NO_EXCEPTION_CLASS));
157         add(Arguments.of(
158             new String[] { "validate-content",
159                 "-m",
160                 "src/test/resources/content/schema-validation-module.xml",
161                 "src/test/resources/content/schema-validation-module-missing-required.xml",
162                 "--as=xml",
163                 "--disable-schema-validation"
164             },
165             // fail due to missing element during parsing
166             ExitCode.FAIL, NO_EXCEPTION_CLASS));
167         add(Arguments.of(
168             new String[] { "validate-content",
169                 "-m",
170                 "src/test/resources/content/schema-validation-module.xml",
171                 "src/test/resources/content/schema-validation-module-missing-required.xml",
172                 "--as=xml",
173                 "--disable-schema-validation",
174                 "--disable-constraint-validation"
175             },
176             ExitCode.OK, NO_EXCEPTION_CLASS));
177         add(Arguments.of(
178             new String[] { "metapath", "list-functions" },
179             ExitCode.OK, NO_EXCEPTION_CLASS));
180         add(Arguments.of(
181             new String[] { "convert",
182                 "-m",
183                 "../core/metaschema/schema/metaschema/metaschema-module-metaschema.xml",
184                 "--to=yaml",
185                 "../core/metaschema/schema/metaschema/metaschema-module-metaschema.xml",
186             },
187             ExitCode.OK, NO_EXCEPTION_CLASS));
188       }
189     };
190     return values.stream();
191   }
192 
193   @ParameterizedTest
194   @MethodSource("providesValues")
195   void testAllCommands(@NonNull String[] args, @NonNull ExitCode expectedExitCode,
196       Class<? extends Throwable> expectedThrownClass) {
197     String[] defaultArgs = { "--show-stack-trace" };
198     String[] fullArgs = Stream.of(args, defaultArgs).flatMap(Stream::of)
199         .toArray(String[]::new);
200     if (expectedThrownClass == null) {
201       evaluateResult(CLI.runCli(fullArgs), expectedExitCode);
202     } else {
203       evaluateResult(CLI.runCli(fullArgs), expectedExitCode, expectedThrownClass);
204     }
205   }
206 
207   @Test
208   void testValidateContent() {
209     try (LogCaptor captor = LogCaptor.forRoot()) {
210       String[] cliArgs = { "validate-content",
211           "-m",
212           "src/test/resources/content/215-module.xml",
213           "src/test/resources/content/215.xml",
214           "--disable-schema-validation"
215       };
216       CLI.runCli(cliArgs);
217       assertThat(captor.getErrorLogs().toString())
218           .contains("expect-default-non-zero: Expect constraint '. > 0' did not match the data",
219               "expect-custom-non-zero: No default message, custom error message for expect-custom-non-zero constraint.",
220               "matches-default-regex-letters-only: Value '1' did not match the pattern",
221               "matches-custom-regex-letters-only: No default message, custom error message for" +
222                   " matches-custom-regex-letters-only constraint.",
223               "cardinality-default-two-minimum: The cardinality '1' is below the required minimum '2' for items" +
224                   " matching",
225               "index-items-default: Index 'index-items-default' has duplicate key for items",
226               "index-items-custom: No default message, custom error message for index-item-custom.",
227               "is-unique-default: Unique constraint violation at paths",
228               "is-unique-custom: No default message, custom error message for is-unique-custom.",
229               "index-has-key-default: Key reference [2] not found in index 'index-items-default' for item",
230               "index-has-key-custom: No default message, custom error message for index-has-key-custom.");
231     }
232   }
233 
234   @Test
235   void testValidateConstraints() {
236     try (LogCaptor captor = LogCaptor.forRoot()) {
237       String[] cliArgs = { "validate",
238           "src/test/resources/content/constraint-example.xml",
239           "-c",
240           "src/test/resources/content/constraint-constraints.xml",
241           "--disable-schema-validation",
242       };
243       CLI.runCli(cliArgs);
244       assertThat(captor.getErrorLogs().toString())
245           .contains("This constraint SHOULD be violated if test passes.");
246     }
247   }
248 }