Add test for JSON data parsing and serializing using a dummy implementation

This commit is contained in:
Paul Schaub 2025-06-03 23:14:23 +02:00
parent ebfde35422
commit cdcbae7e5f
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
4 changed files with 208 additions and 29 deletions

View file

@ -45,7 +45,8 @@ class SopVCLI {
@JvmField var EXECUTABLE_NAME = "sopv"
@JvmField
@CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT)
@CommandLine.Option(
names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT)
var stacktrace = false
@JvmStatic

View file

@ -10,6 +10,15 @@ import sop.enums.SignatureMode
import sop.util.Optional
import sop.util.UTCUtil
/**
* Metadata about a verified signature.
*
* @param creationTime creation time of the signature
* @param signingKeyFingerprint fingerprint of the (sub-)key that issued the signature
* @param signingCertFingerprint fingerprint of the certificate that contains the signing key
* @param signatureMode optional signature mode (text/binary)
* @param jsonOrDescription arbitrary text or JSON data
*/
data class Verification(
val creationTime: Date,
val signingKeyFingerprint: String,
@ -47,27 +56,28 @@ data class Verification(
Optional.ofNullable(signatureMode),
Optional.of(jsonSerializer.serialize(json)))
@Deprecated("Replaced by jsonOrDescription",
replaceWith = ReplaceWith("jsonOrDescription")
)
@Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription"))
val description = jsonOrDescription
/** This value is `true` if the [Verification] contains extension JSON. */
val containsJson: Boolean =
jsonOrDescription.get()?.trim()?.let { it.startsWith("{") && it.endsWith("}") } ?: false
/**
* Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result.
* This method returns `null` if parsing fails.
* Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the
* result. This method returns `null` if parsing fails.
*
* @param parser [JSONParser] implementation
* @return successfully parsed [JSON] POJO or `null`.
*/
fun getJson(parser: JSONParser): JSON? {
return jsonOrDescription.get()
?.let {
try {
parser.parse(it)
} catch (e: ParseException) {
null
}
return jsonOrDescription.get()?.let {
try {
parser.parse(it)
} catch (e: ParseException) {
null
}
}
}
override fun toString(): String =
@ -116,35 +126,37 @@ data class Verification(
/**
* POJO data class representing JSON metadata.
*
* @param signers list of supplied CERTS objects that could have issued the signature, identified by
* the name given on the command line.
* @param signers list of supplied CERTS objects that could have issued the signature,
* identified by the name given on the command line.
* @param comment a freeform UTF-8 encoded text describing the verification
* @param ext an extension object containing arbitrary, implementation-specific data
*/
data class JSON(
val signers: List<String>,
val comment: String?,
val ext: Any?)
data class JSON(val signers: List<String>, val comment: String?, val ext: Any?) {
/**
* Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings.
*/
/** Create a JSON object with only a list of signers. */
constructor(signers: List<String>) : this(signers, null, null)
/** Create a JSON object with only a single signer. */
constructor(signer: String) : this(listOf(signer))
}
/** Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. */
fun interface JSONParser {
/**
* Parse a [JSON] POJO from the given single-line [string].
* If the string does not represent a JSON object matching the [JSON] definition,
* this method throws a [ParseException].
* Parse a [JSON] POJO from the given single-line [string]. If the string does not represent
* a JSON object matching the [JSON] definition, this method throws a [ParseException].
*
* @param string [String] representation of the [JSON] object.
* @return parsed [JSON] POJO
* @throws ParseException if the [string] is not a JSON string representing the [JSON] object.
* @throws ParseException if the [string] is not a JSON string representing the [JSON]
* object.
*/
@Throws(ParseException::class)
fun parse(string: String): JSON
@Throws(ParseException::class) fun parse(string: String): JSON
}
/**
* Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings.
* Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON
* strings.
*/
fun interface JSONSerializer {

View file

@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import sop.enums.SignatureMode;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class VerificationJSONTest {
// A hacky self-made "JSON parser" stand-in.
// Only used for testing, do not use in production!
private Verification.JSONParser dummyParser = new Verification.JSONParser() {
@NotNull
@Override
public Verification.JSON parse(@NotNull String string) throws ParseException {
if (!string.startsWith("{")) {
throw new ParseException("Alleged JSON String does not begin with '{'", 0);
}
if (!string.endsWith("}")) {
throw new ParseException("Alleged JSON String does not end with '}'", string.length() - 1);
}
List<String> signersList = new ArrayList<>();
Matcher signersMat = Pattern.compile("\"signers\": \\[(.*?)\\]").matcher(string);
if (signersMat.find()) {
String signersCat = signersMat.group(1);
String[] split = signersCat.split(",");
for (String s : split) {
s = s.trim();
signersList.add(s.substring(1, s.length() - 1));
}
}
String comment = null;
Matcher commentMat = Pattern.compile("\"comment\": \"(.*?)\"").matcher(string);
if (commentMat.find()) {
comment = commentMat.group(1);
}
String ext = null;
Matcher extMat = Pattern.compile("\"ext\": (.*?})}").matcher(string);
if (extMat.find()) {
ext = extMat.group(1);
}
return new Verification.JSON(signersList, comment, ext);
}
};
// A just as hacky "JSON Serializer" lookalike.
// Also don't use in production, for testing only!
private Verification.JSONSerializer dummySerializer = new Verification.JSONSerializer() {
@NotNull
@Override
public String serialize(@NotNull Verification.JSON json) {
if (json.getSigners().isEmpty() && json.getComment() == null && json.getExt() == null) {
return "";
}
StringBuilder sb = new StringBuilder("{");
boolean comma = false;
if (!json.getSigners().isEmpty()) {
comma = true;
sb.append("\"signers\": [");
for (Iterator<String> iterator = json.getSigners().iterator(); iterator.hasNext(); ) {
String signer = iterator.next();
sb.append("\"").append(signer).append("\"");
if (iterator.hasNext()) {
sb.append(", ");
}
}
sb.append("]");
}
if (json.getComment() != null) {
if (comma) {
sb.append(", ");
}
comma = true;
sb.append("\"comment\": \"").append(json.getComment()).append("\"");
}
if (json.getExt() != null) {
if (comma) {
sb.append(", ");
}
comma = true;
sb.append("\"ext\": ").append(json.getExt().toString());
}
return sb.append("}").toString();
}
};
@Test
public void testSimpleSerializeParse() throws ParseException {
String signer = "alice.pub";
Verification.JSON json = new Verification.JSON(signer);
String string = dummySerializer.serialize(json);
assertEquals("{\"signers\": [\"alice.pub\"]}", string);
Verification.JSON parsed = dummyParser.parse(string);
assertEquals(signer, parsed.getSigners().get(0));
assertEquals(1, parsed.getSigners().size());
assertNull(parsed.getComment());
assertNull(parsed.getExt());
}
@Test
public void testAdvancedSerializeParse() throws ParseException {
Verification.JSON json = new Verification.JSON(
Arrays.asList("../certs/alice.pub", "/etc/pgp/certs.pgp"),
"This is a comment",
"{\"Foo\": \"Bar\"}");
String serialized = dummySerializer.serialize(json);
assertEquals("{\"signers\": [\"../certs/alice.pub\", \"/etc/pgp/certs.pgp\"], \"comment\": \"This is a comment\", \"ext\": {\"Foo\": \"Bar\"}}",
serialized);
Verification.JSON parsed = dummyParser.parse(serialized);
assertEquals(json.getSigners(), parsed.getSigners());
assertEquals(json.getComment(), parsed.getComment());
assertEquals(json.getExt(), parsed.getExt());
}
@Test
public void testVerificationWithSimpleJson() {
String string = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E mode:text {\"signers\": [\"alice.pgp\"]}";
Verification verification = Verification.fromString(string);
assertTrue(verification.getContainsJson());
assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningKeyFingerprint());
assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningCertFingerprint());
assertEquals(SignatureMode.text, verification.getSignatureMode().get());
Verification.JSON json = verification.getJson(dummyParser);
assertNotNull(json, "The verification string MUST contain valid extension json");
assertEquals(Collections.singletonList("alice.pgp"), json.getSigners());
assertNull(json.getComment());
assertNull(json.getExt());
verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer);
assertEquals(string, verification.toString());
}
}

View file

@ -13,6 +13,7 @@ import java.text.ParseException;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class VerificationTest {
@ -25,6 +26,8 @@ public class VerificationTest {
Verification verification = new Verification(signDate, keyFP, certFP);
assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString());
assertFalse(verification.getContainsJson());
VerificationAssert.assertThatVerification(verification)
.issuedBy(certFP)
.isBySigningKey(keyFP)