From cf284be1fbc4f86422f8e99ee32268d6a190cce0 Mon Sep 17 00:00:00 2001 From: eskopp Date: Tue, 6 Oct 2026 21:22:41 +0200 Subject: [PATCH 1/2] Remove System.exit from main and error paths, add tests for main and failures --- src/main/java/javacrypt/MyCryptMain.java | 34 ++++--- src/main/java/javacrypt/RunEncrypt.java | 4 +- src/main/java/javacrypt/RunGenKeys.java | 6 +- src/test/java/javacrypt/ErrorPathTest.java | 107 +++++++++++++++++++++ src/test/java/javacrypt/MainTest.java | 79 +++++++++++++++ 5 files changed, 208 insertions(+), 22 deletions(-) create mode 100644 src/test/java/javacrypt/ErrorPathTest.java create mode 100644 src/test/java/javacrypt/MainTest.java diff --git a/src/main/java/javacrypt/MyCryptMain.java b/src/main/java/javacrypt/MyCryptMain.java index 6b5f663..a20632a 100644 --- a/src/main/java/javacrypt/MyCryptMain.java +++ b/src/main/java/javacrypt/MyCryptMain.java @@ -36,7 +36,7 @@ public class MyCryptMain { ); /** - * Displays the usage of the program and exits. + * Displays the usage of the program. */ private static void usage() { String[] msg = { @@ -51,9 +51,6 @@ private static void usage() { // Output the strings Arrays.stream(msg).forEach(System.err::println); - - // Exit the program with an error code - System.exit(0); } /** @@ -63,24 +60,30 @@ private static void usage() { * @throws Exception If an error occurs. */ public static void main(String[] args) throws Exception { + int exitCode = execute(args); + if (exitCode != 0) { + System.exit(exitCode); + } + } + + /** + * Runs the program without terminating the JVM. + * + * @param args The command-line arguments. + * @return The exit code: 0 on success, 1 if the usage was wrong. + * @throws Exception If an error occurs. + */ + static int execute(String[] args) throws Exception { // Factory instance RunnableKeyValueFactory runnableKeyValueFactory = new RunnableKeyValueFactory(MY_ARRAY.toArray(new String[0][])); - // Get the number of command-line arguments - int argSize = args.length; - - if (argSize == 0) { - usage(); // Static method + if (args.length == 0 || !runnableKeyValueFactory.containsKey(args[0])) { + usage(); + return 1; } - // Get the control argument String cmdKey = args[0]; - // Check if the command exists - if (!runnableKeyValueFactory.containsKey(cmdKey)) { - usage(); - } - // Perform the shift operation List optArgList = Arrays.asList(args).subList(1, args.length); @@ -91,5 +94,6 @@ public static void main(String[] args) throws Exception { myRun.run(optArgList); System.out.println("End of the program."); + return 0; } } diff --git a/src/main/java/javacrypt/RunEncrypt.java b/src/main/java/javacrypt/RunEncrypt.java index dcfc851..b7d865f 100644 --- a/src/main/java/javacrypt/RunEncrypt.java +++ b/src/main/java/javacrypt/RunEncrypt.java @@ -87,9 +87,7 @@ protected Cipher getCipher(Key key) { cipher.init(Cipher.ENCRYPT_MODE, pubKey); return cipher; } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException ex) { - System.err.println("Exception: " + ex.getMessage()); - System.exit(8); - return null; // Unreachable code, added to satisfy the compiler + throw new IllegalStateException("Cannot create cipher: " + ex.getMessage(), ex); } } diff --git a/src/main/java/javacrypt/RunGenKeys.java b/src/main/java/javacrypt/RunGenKeys.java index 273c1e2..d6ad1aa 100644 --- a/src/main/java/javacrypt/RunGenKeys.java +++ b/src/main/java/javacrypt/RunGenKeys.java @@ -85,14 +85,12 @@ public void run(List args) { * * @param key The key to write. * @param file The file to write the key to. + * @throws IOException If the file cannot be written. */ - private void writeKeyToFile(Key key, File file) { + private void writeKeyToFile(Key key, File file) throws IOException { try (FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos)) { oos.writeObject(key); - } catch (IOException e) { - System.err.println("Exception(writeKeyToFile): " + e.getMessage()); - System.exit(2); } } diff --git a/src/test/java/javacrypt/ErrorPathTest.java b/src/test/java/javacrypt/ErrorPathTest.java new file mode 100644 index 0000000..d672b0e --- /dev/null +++ b/src/test/java/javacrypt/ErrorPathTest.java @@ -0,0 +1,107 @@ +package javacrypt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class ErrorPathTest { + + @TempDir + Path dir; + + private final PrintStream originalErr = System.err; + private final ByteArrayOutputStream err = new ByteArrayOutputStream(); + + @BeforeEach + void captureErr() { + System.setErr(new PrintStream(err, true, StandardCharsets.UTF_8)); + } + + @AfterEach + void restoreErr() { + System.setErr(originalErr); + } + + private String errText() { + return new String(err.toByteArray(), StandardCharsets.UTF_8); + } + + @Test + void encryptWithMissingKeyFileReportsErrorAndWritesNothing() { + Path out = dir.resolve("out.dat"); + + new RunEncrypt().run(Arrays.asList( + dir.resolve("missing.key").toString(), dir.resolve("in.dat").toString(), out.toString())); + + assertTrue(errText().contains("EXCEPTION: run")); + assertFalse(Files.exists(out)); + } + + @Test + void decryptWithMissingInputReportsError() { + Path priv = dir.resolve("priv.key"); + new RunGenKeys().run(Arrays.asList(priv.toString(), dir.resolve("pub.key").toString())); + + new RunDecrypt().run(Arrays.asList( + priv.toString(), dir.resolve("missing.dat").toString(), dir.resolve("out.dat").toString())); + + assertTrue(errText().contains("EXCEPTION: run")); + } + + @Test + void decryptWithWrongKeyFails() throws Exception { + Path priv1 = dir.resolve("priv1.key"); + Path pub1 = dir.resolve("pub1.key"); + Path priv2 = dir.resolve("priv2.key"); + Path pub2 = dir.resolve("pub2.key"); + new RunGenKeys().run(Arrays.asList(priv1.toString(), pub1.toString())); + new RunGenKeys().run(Arrays.asList(priv2.toString(), pub2.toString())); + Path in = dir.resolve("in.txt"); + Path enc = dir.resolve("enc.dat"); + Path dec = dir.resolve("dec.txt"); + Files.write(in, "secret".getBytes(StandardCharsets.UTF_8)); + + new RunEncrypt().run(Arrays.asList(pub1.toString(), in.toString(), enc.toString())); + new RunDecrypt().run(Arrays.asList(priv2.toString(), enc.toString(), dec.toString())); + + assertTrue(errText().contains("Exception")); + assertEquals(0, Files.exists(dec) ? Files.size(dec) : 0); + } + + @Test + void genKeysToUnwritablePathReportsErrorWithoutExit() { + Path bad = dir.resolve("no-such-dir").resolve("priv.key"); + + new RunGenKeys().run(Arrays.asList(bad.toString(), dir.resolve("pub.key").toString())); + + assertTrue(errText().contains("IOException")); + assertFalse(Files.exists(dir.resolve("pub.key"))); + } + + @Test + void copyWithMissingInputReportsError() { + new RunCopy().run(Arrays.asList(dir.resolve("missing").toString(), dir.resolve("out").toString())); + + assertTrue(errText().contains("Exception")); + } + + @Test + void genKeysDoesNotSupportCrypt() { + RunGenKeys genKeys = new RunGenKeys(); + + assertThrows(UnsupportedOperationException.class, () -> genKeys.crypt(new byte[0], null, null)); + assertEquals(100, genKeys.getCryptoBufSize()); + } +} diff --git a/src/test/java/javacrypt/MainTest.java b/src/test/java/javacrypt/MainTest.java new file mode 100644 index 0000000..29dc882 --- /dev/null +++ b/src/test/java/javacrypt/MainTest.java @@ -0,0 +1,79 @@ +package javacrypt; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class MainTest { + + @TempDir + Path dir; + + private final PrintStream originalErr = System.err; + private final ByteArrayOutputStream err = new ByteArrayOutputStream(); + + @BeforeEach + void captureErr() { + System.setErr(new PrintStream(err, true, StandardCharsets.UTF_8)); + } + + @AfterEach + void restoreErr() { + System.setErr(originalErr); + } + + private String errText() { + return new String(err.toByteArray(), StandardCharsets.UTF_8); + } + + @Test + void noArgumentsPrintsUsageAndFails() throws Exception { + assertEquals(1, MyCryptMain.execute(new String[0])); + assertTrue(errText().contains("Usage:")); + } + + @Test + void unknownCommandPrintsUsageAndFails() throws Exception { + assertEquals(1, MyCryptMain.execute(new String[]{"-bogus"})); + assertTrue(errText().contains("Usage:")); + } + + @Test + void fullCycleThroughMain() throws Exception { + Path priv = dir.resolve("priv.key"); + Path pub = dir.resolve("pub.key"); + Path in = dir.resolve("in.txt"); + Path enc = dir.resolve("enc.dat"); + Path dec = dir.resolve("dec.txt"); + byte[] plain = "A text that is clearly longer than one hundred bytes so that several RSA blocks are needed for it. Yes." + .getBytes(StandardCharsets.UTF_8); + Files.write(in, plain); + + assertEquals(0, MyCryptMain.execute(new String[]{"-genkeys", priv.toString(), pub.toString()})); + assertEquals(0, MyCryptMain.execute(new String[]{"-encrypt", pub.toString(), in.toString(), enc.toString()})); + assertEquals(0, MyCryptMain.execute(new String[]{"-decrypt", priv.toString(), enc.toString(), dec.toString()})); + + assertArrayEquals(plain, Files.readAllBytes(dec)); + } + + @Test + void copyThroughMain() throws Exception { + Path in = dir.resolve("in.txt"); + Path out = dir.resolve("out.txt"); + Files.write(in, new byte[]{1, 2, 3}); + + assertEquals(0, MyCryptMain.execute(new String[]{"-copy", in.toString(), out.toString()})); + + assertArrayEquals(new byte[]{1, 2, 3}, Files.readAllBytes(out)); + } +} From ea2d3cb09c1025d31a72365a6d0a93efefa6c711 Mon Sep 17 00:00:00 2001 From: eskopp Date: Tue, 6 Oct 2026 21:31:00 +0200 Subject: [PATCH 2/2] CI: expect exit code 1 when started without arguments --- .github/workflows/test.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96fb4d3..14b045f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,8 +43,15 @@ jobs: name: jacoco-report path: target/site/jacoco - - name: Start Tool - run: java -jar target/JavaCrypt.jar + - name: Start Tool (usage must exit with code 1) + run: | + set +e + java -jar target/JavaCrypt.jar + code=$? + if [ "$code" -ne 1 ]; then + echo "Expected exit code 1, got $code" + exit 1 + fi - name: Generate Keys run: java -jar target/JavaCrypt.jar -genkeys priv.key pub.key