Skip to content

Commit

Permalink
Add lots of mocked tests to sop-java-picocli
Browse files Browse the repository at this point in the history
  • Loading branch information
vanitasvitae committed Jul 11, 2021
1 parent f537868 commit 30e70c8
Show file tree
Hide file tree
Showing 15 changed files with 732 additions and 43 deletions.
7 changes: 6 additions & 1 deletion sop-java-picocli/build.gradle
Expand Up @@ -4,12 +4,17 @@ plugins {

dependencies {
implementation(project(":sop-java"))
implementation 'info.picocli:picocli:4.5.2'
implementation 'info.picocli:picocli:4.6.1'
implementation 'com.google.inject:guice:5.0.1'

testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"

// https://todd.ginsberg.com/post/testing-system-exit/
testImplementation 'com.ginsberg:junit5-system-exit:1.1.1'

testImplementation "org.mockito:mockito-core:3.11.2"

// https://mvnrepository.com/artifact/com.google.code.findbugs/jsr305
implementation group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2'
}
Expand Down
Expand Up @@ -26,16 +26,16 @@ public class DateParser {
private static final TimeZone tz = TimeZone.getTimeZone("UTC");
private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");

private static final Date beginningOfTime = new Date(0);
private static final Date endOfTime = new Date(8640000000000000L);
public static final Date BEGINNING_OF_TIME = new Date(0);
public static final Date END_OF_TIME = new Date(8640000000000000L);

static {
df.setTimeZone(tz);
}

public static Date parseNotAfter(String notAfter) {
try {
return notAfter.equals("now") ? new Date() : notAfter.equals("-") ? endOfTime : df.parse(notAfter);
return notAfter.equals("now") ? new Date() : notAfter.equals("-") ? END_OF_TIME : df.parse(notAfter);
} catch (ParseException e) {
Print.errln("Invalid date string supplied as value of --not-after.");
Print.trace(e);
Expand All @@ -46,7 +46,7 @@ public static Date parseNotAfter(String notAfter) {

public static Date parseNotBefore(String notBefore) {
try {
return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? beginningOfTime : df.parse(notBefore);
return notBefore.equals("now") ? new Date() : notBefore.equals("-") ? BEGINNING_OF_TIME : df.parse(notBefore);
} catch (ParseException e) {
Print.errln("Invalid date string supplied as value of --not-before.");
Print.trace(e);
Expand Down
16 changes: 14 additions & 2 deletions sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java
Expand Up @@ -27,7 +27,8 @@
import sop.cli.picocli.commands.VerifyCmd;
import sop.cli.picocli.commands.VersionCmd;

@CommandLine.Command(exitCodeOnInvalidInput = 69,
@CommandLine.Command(
exitCodeOnInvalidInput = 69,
subcommands = {
ArmorCmd.class,
DearmorCmd.class,
Expand All @@ -42,7 +43,14 @@
)
public class SopCLI {

public static SOP SOP_INSTANCE;
static SOP SOP_INSTANCE;

public static void main(String[] args) {
int exitCode = execute(args);
if (exitCode != 0) {
System.exit(exitCode);
}
}

public static int execute(String[] args) {
return new CommandLine(SopCLI.class).execute(args);
Expand All @@ -58,4 +66,8 @@ public static SOP getSop() {
public static void setSopInstance(SOP instance) {
SOP_INSTANCE = instance;
}

static void assureSOPInstanceIsNull() {
SOP_INSTANCE = null;
}
}
Expand Up @@ -54,7 +54,6 @@ public void run() {
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Armor labels not supported.");
System.exit(unsupportedOption.getExitCode());
return;
}
}

Expand Down
Expand Up @@ -34,6 +34,10 @@ public void run() {
.dearmor()
.data(System.in)
.writeTo(System.out);
} catch (SOPGPException.BadData e) {
Print.errln("Bad data.");
Print.trace(e);
System.exit(e.getExitCode());
} catch (IOException e) {
Print.errln("IO Error.");
Print.trace(e);
Expand Down
Expand Up @@ -20,30 +20,29 @@
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Pattern;

import picocli.CommandLine;
import sop.DecryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.Verification;
import sop.cli.picocli.DateParser;
import sop.cli.picocli.Print;
import sop.cli.picocli.SopCLI;
import sop.exception.SOPGPException;
import sop.operation.Decrypt;
import sop.util.HexUtil;

@CommandLine.Command(name = "decrypt",
description = "Decrypt a message from standard input",
exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE)
public class DecryptCmd implements Runnable {

private static final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");

@CommandLine.Option(
names = {"--session-key-out"},
description = "Can be used to learn the session key on successful decryption",
Expand Down Expand Up @@ -101,7 +100,6 @@ public void run() {
setNotBefore(notBefore, decrypt);
setWithPasswords(withPassword, decrypt);
setWithSessionKeys(withSessionKey, decrypt);
setSessionKeyOut(sessionKeyOut, decrypt);
setVerifyWith(certs, decrypt);
setDecryptWith(keys, decrypt);

Expand All @@ -126,6 +124,20 @@ public void run() {
}
}
}
if (verifyOut != null) {
if (!verifyOut.createNewFile()) {
throw new IOException("Cannot create file " + verifyOut.getAbsolutePath());
}
try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) {
PrintWriter writer = new PrintWriter(outputStream);
for (Verification verification : result.getVerifications()) {
// CHECKSTYLE:OFF
writer.println(verification.toString());
// CHECKSTYLE:ON
}
writer.flush();
}
}
} catch (SOPGPException.BadData badData) {
Print.errln("No valid OpenPGP message found on Standard Input.");
Print.trace(badData);
Expand All @@ -142,6 +154,10 @@ public void run() {
Print.errln("No verifiable signature found.");
Print.trace(noSignature);
System.exit(noSignature.getExitCode());
} catch (SOPGPException.CannotDecrypt cannotDecrypt) {
Print.errln("Cannot decrypt.");
Print.trace(cannotDecrypt);
System.exit(cannotDecrypt.getExitCode());
}
}

Expand Down Expand Up @@ -206,21 +222,17 @@ private void unlinkExistingVerifyOut(File verifyOut) {
}
}

private void setSessionKeyOut(File sessionKeyOut, Decrypt decrypt) {
if (sessionKeyOut == null) {
return;
}

Print.errln("Unsupported option '--session-key-out'.");
System.exit(SOPGPException.UnsupportedOption.EXIT_CODE);
}

private void setWithSessionKeys(List<String> withSessionKey, Decrypt decrypt) {
Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$");
for (String sessionKey : withSessionKey) {
byte[] bytes = sessionKey.getBytes(StandardCharsets.UTF_8);
byte algorithm = bytes[0];
byte[] key = new byte[bytes.length - 1];
System.arraycopy(bytes, 1, key, 0, key.length);
if (!sessionKeyPattern.matcher(sessionKey).matches()) {
Print.errln("Invalid session key format.");
Print.errln("Session keys are expected in the format 'ALGONUM:HEXKEY'");
System.exit(1);
}
String[] split = sessionKey.split(":");
byte algorithm = (byte) Integer.parseInt(split[0]);
byte[] key = HexUtil.hexToBytes(split[1]);

try {
decrypt.withSessionKey(new SessionKey(algorithm, key));
Expand Down Expand Up @@ -250,32 +262,24 @@ private void setWithPasswords(List<String> withPassword, Decrypt decrypt) {
}

private void setNotAfter(String notAfter, Decrypt decrypt) {
if (notAfter == null) {
return;
}

Date notAfterDate = DateParser.parseNotAfter(notAfter);
try {
decrypt.verifyNotAfter(notAfterDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Option '--not-after' not supported.");
Print.trace(unsupportedOption);
// System.exit(unsupportedOption.getExitCode());
System.exit(unsupportedOption.getExitCode());
}
}

private void setNotBefore(String notBefore, Decrypt decrypt) {
if (notBefore == null) {
return;
}

Date notBeforeDate = DateParser.parseNotBefore(notBefore);
try {
decrypt.verifyNotBefore(notBeforeDate);
} catch (SOPGPException.UnsupportedOption unsupportedOption) {
Print.errln("Option '--not-before' not supported.");
Print.trace(unsupportedOption);
// System.exit(unsupportedOption.getExitCode());
System.exit(unsupportedOption.getExitCode());
}
}
}
130 changes: 130 additions & 0 deletions sop-java-picocli/src/test/java/sop/cli/picocli/ArmorCmdTest.java
@@ -0,0 +1,130 @@
/*
* Copyright 2021 Paul Schaub.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sop.cli.picocli;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.OutputStream;

import com.ginsberg.junit.exit.ExpectSystemExitWithStatus;
import com.ginsberg.junit.exit.FailOnSystemExit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import sop.Ready;
import sop.SOP;
import sop.enums.ArmorLabel;
import sop.exception.SOPGPException;
import sop.operation.Armor;

public class ArmorCmdTest {

private Armor armor;
private SOP sop;

@BeforeEach
public void mockComponents() throws SOPGPException.BadData {
armor = mock(Armor.class);
sop = mock(SOP.class);
when(sop.armor()).thenReturn(armor);
when(armor.data(any())).thenReturn(nopReady());

SopCLI.setSopInstance(sop);
}

@Test
public void assertAllowNestedIsCalledWhenFlagged() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"armor", "--allow-nested"});
verify(armor, times(1)).allowNested();
}

@Test
public void assertAllowNestedIsNotCalledByDefault() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"armor"});
verify(armor, never()).allowNested();
}

@Test
public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption {
SopCLI.main(new String[] {"armor"});
verify(armor, never()).label(any());
}

@Test
public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption {
for (ArmorLabel label : ArmorLabel.values()) {
SopCLI.main(new String[] {"armor", "--label", label.name()});
verify(armor, times(1)).label(label);
}
}

@Test
public void assertDataIsAlwaysCalled() throws SOPGPException.BadData {
SopCLI.main(new String[] {"armor"});
verify(armor, times(1)).data(any());
}

@Test
@ExpectSystemExitWithStatus(37)
public void assertThrowsForInvalidLabel() {
SopCLI.main(new String[] {"armor", "--label", "Invalid"});
}

@Test
@ExpectSystemExitWithStatus(37)
public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption {
when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption());

SopCLI.main(new String[] {"armor", "--label", "Sig"});
}

@Test
@ExpectSystemExitWithStatus(37)
public void ifAllowNestedUnsupportedExit37() throws SOPGPException.UnsupportedOption {
when(armor.allowNested()).thenThrow(new SOPGPException.UnsupportedOption());

SopCLI.main(new String[] {"armor", "--allow-nested"});
}

@Test
@ExpectSystemExitWithStatus(41)
public void ifBadDataExit41() throws SOPGPException.BadData {
when(armor.data(any())).thenThrow(new SOPGPException.BadData(new IOException()));

SopCLI.main(new String[] {"armor"});
}

@Test
@FailOnSystemExit
public void ifNoErrorsNoExit() {
when(sop.armor()).thenReturn(armor);

SopCLI.main(new String[] {"armor"});
}

private static Ready nopReady() {
return new Ready() {
@Override
public void writeTo(OutputStream outputStream) {
}
};
}
}

0 comments on commit 30e70c8

Please sign in to comment.