diff options
| author | saturneric <[email protected]> | 2026-09-20 22:21:55 +0200 |
|---|---|---|
| committer | saturneric <[email protected]> | 2026-09-20 22:21:55 +0200 |
| commit | 8cacac12288d22a7c81058b1b765bd24e56612f8 (patch) | |
| tree | 72fc6c0fed3b5c216d43e77fd1cb536278211413 /src/m_pgp_inspect/test/PGPInspectModelTest.cpp | |
| parent | build(cmake): organize module output directories (diff) | |
| download | modules-8cacac12288d22a7c81058b1b765bd24e56612f8.tar.gz modules-8cacac12288d22a7c81058b1b765bd24e56612f8.zip | |
feat(pgp): add OpenPGP structure inspector
- add sandbox-compatible module for inspecting packet structure
- render armor, cleartext, packet fields, errors, and searchable trees
- parse inspector JSON with nested packet support and summaries
- add model tests covering parsing, structure detection, and formatting
- add module metadata, build integration, and translation catalogs
Diffstat (limited to 'src/m_pgp_inspect/test/PGPInspectModelTest.cpp')
| -rw-r--r-- | src/m_pgp_inspect/test/PGPInspectModelTest.cpp | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/src/m_pgp_inspect/test/PGPInspectModelTest.cpp b/src/m_pgp_inspect/test/PGPInspectModelTest.cpp new file mode 100644 index 0000000..3815176 --- /dev/null +++ b/src/m_pgp_inspect/test/PGPInspectModelTest.cpp @@ -0,0 +1,298 @@ +/** + * Copyright (C) 2021-2024 Saturneric <[email protected]> + * + * This file is part of GpgFrontend. + * + * GpgFrontend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GpgFrontend 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with GpgFrontend. If not, see <https://www.gnu.org/licenses/>. + * + * The initial version of the source code is inherited from + * the gpg4usb project, which is under GPL-3.0-or-later. + * + * All the source code of GpgFrontend was modified and released by + * Saturneric <[email protected]> starting on May 12, 2021. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include <gtest/gtest.h> + +#include <QCoreApplication> + +#include "PGPInspectModel.h" + +namespace { + +/// A minimal but complete document, in the shape the Rust walk emits. +constexpr const char* kDocument = R"({ + "format": "armored", + "size": 325, + "errors": [], + "blocks": [ + { + "kind": "armored", + "offset": 0, + "armor": { + "blockType": "PGP SIGNATURE", + "headers": [{"label": "Version", "value": "GnuPG v2"}], + "crc24": "ok" + }, + "packets": [ + { + "tag": 8, + "tagName": "Compressed Data", + "offset": 0, + "headerVersion": "new", + "headerLength": 2, + "bodyLength": 189, + "lengthType": "fixed", + "fields": [{"label": "Compression Algorithm", "value": "ZIP"}], + "children": [ + { + "tag": 2, + "tagName": "Signature", + "offset": 0, + "headerVersion": "old", + "headerLength": 3, + "bodyLength": 100, + "lengthType": "fixed", + "version": 4, + "fields": [{"label": "Hash Algorithm", "value": "Sha256"}], + "children": [], + "error": null + } + ], + "error": null + } + ], + "error": null + } + ] +})"; + +} // namespace + +// -- reading the document ---------------------------------------------------- + +TEST(PGPInspectModelTest, ReadsTheDocumentShape) { + const auto document = ParsePGPInspectDocument(kDocument); + + ASSERT_TRUE(document.valid); + EXPECT_EQ(document.format, QString("armored")); + EXPECT_EQ(document.size, 325); + ASSERT_EQ(document.blocks.size(), 1); + EXPECT_TRUE(document.errors.isEmpty()); +} + +TEST(PGPInspectModelTest, ReadsTheArmorEnvelope) { + const auto document = ParsePGPInspectDocument(kDocument); + const auto& block = document.blocks.at(0); + + ASSERT_TRUE(block.has_armor); + EXPECT_EQ(block.armor.block_type, QString("PGP SIGNATURE")); + EXPECT_EQ(block.armor.crc24, QString("ok")); + ASSERT_EQ(block.armor.headers.size(), 1); + EXPECT_EQ(block.armor.headers.at(0).label, QString("Version")); +} + +TEST(PGPInspectModelTest, NestsChildrenUnderTheirContainer) { + // The recursion is the reason the tree exists; a flat read would lose it. + const auto document = ParsePGPInspectDocument(kDocument); + const auto& packets = document.blocks.at(0).packets; + + ASSERT_EQ(packets.size(), 1); + EXPECT_EQ(packets.at(0).tag, 8); + ASSERT_EQ(packets.at(0).children.size(), 1); + EXPECT_EQ(packets.at(0).children.at(0).tag, 2); + EXPECT_EQ(packets.at(0).children.at(0).version, 4); +} + +TEST(PGPInspectModelTest, CountsNestedPacketsToo) { + EXPECT_EQ(ParsePGPInspectDocument(kDocument).PacketCount(), 2); +} + +TEST(PGPInspectModelTest, KeepsTheFramingFiguresVerbatim) { + const auto& packet = + ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0); + + EXPECT_EQ(packet.offset, 0); + EXPECT_EQ(packet.header_length, 2); + EXPECT_EQ(packet.body_length, 189); + EXPECT_EQ(packet.header_version, QString("new")); + EXPECT_EQ(packet.length_type, QString("fixed")); +} + +TEST(PGPInspectModelTest, AbsentVersionReadsAsMinusOne) { + // Most container packets carry no version octet. That is a fact about the + // type, so it must not look like a defect in the document. + const auto& packet = + ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0); + EXPECT_EQ(packet.version, -1); + EXPECT_FALSE(packet.Malformed()); +} + +// -- the shapes that are not the happy one ------------------------------------ + +TEST(PGPInspectModelTest, MalformedJsonIsReportedNotThrown) { + const auto document = ParsePGPInspectDocument("{ this is not json"); + + EXPECT_FALSE(document.valid); + EXPECT_FALSE(document.parse_error.isEmpty()); + EXPECT_TRUE(document.blocks.isEmpty()); +} + +TEST(PGPInspectModelTest, EmptyInputIsReportedNotThrown) { + const auto document = ParsePGPInspectDocument({}); + + EXPECT_FALSE(document.valid); + EXPECT_FALSE(document.parse_error.isEmpty()); +} + +TEST(PGPInspectModelTest, ANonObjectDocumentIsRefused) { + EXPECT_FALSE(ParsePGPInspectDocument("[1, 2, 3]").valid); +} + +TEST(PGPInspectModelTest, AnEmptyDocumentIsValidAndEmpty) { + // "nothing recognisable here" is a finding the inspector reports, not a + // failure to read its answer. + const auto document = ParsePGPInspectDocument( + R"({"format":"binary","size":4,"blocks":[],"errors":["not OpenPGP data"]})"); + + ASSERT_TRUE(document.valid); + EXPECT_TRUE(document.blocks.isEmpty()); + ASSERT_EQ(document.errors.size(), 1); + EXPECT_EQ(document.errors.at(0), QString("not OpenPGP data")); +} + +TEST(PGPInspectModelTest, APacketErrorSurvivesIntoTheTree) { + const auto document = ParsePGPInspectDocument( + R"({"blocks":[{"packets":[{"tag":2,"error":"the packet body is truncated"}]}]})"); + + ASSERT_EQ(document.blocks.size(), 1); + const auto& packet = document.blocks.at(0).packets.at(0); + EXPECT_TRUE(packet.Malformed()); + EXPECT_EQ(packet.error, QString("the packet body is truncated")); +} + +TEST(PGPInspectModelTest, MissingFieldsTakeTheirDefaults) { + // A document from an older or newer engine must degrade field by field + // rather than costing the reader the whole tree. + const auto document = + ParsePGPInspectDocument(R"({"blocks":[{"packets":[{}]}]})"); + + ASSERT_TRUE(document.valid); + const auto& packet = document.blocks.at(0).packets.at(0); + EXPECT_EQ(packet.tag, -1); + EXPECT_TRUE(packet.tag_name.isEmpty()); + EXPECT_EQ(packet.body_length, 0); +} + +TEST(PGPInspectModelTest, ADetailRowWithNeitherHalfIsDropped) { + const auto document = ParsePGPInspectDocument( + R"({"blocks":[{"packets":[{"fields":[{},{"label":"A","value":"B"}]}]}]})"); + + const auto& fields = document.blocks.at(0).packets.at(0).fields; + ASSERT_EQ(fields.size(), 1); + EXPECT_EQ(fields.at(0).label, QString("A")); +} + +TEST(PGPInspectModelTest, ReadsTheCleartextHalf) { + const auto document = ParsePGPInspectDocument( + R"({"format":"cleartext","blocks":[{"kind":"cleartext","cleartext":{"textSize":42,"headers":[{"label":"Hash","value":"SHA256"}]},"packets":[]}]})"); + + const auto& block = document.blocks.at(0); + ASSERT_TRUE(block.has_cleartext); + EXPECT_EQ(block.cleartext_text_size, 42); + ASSERT_EQ(block.cleartext_headers.size(), 1); + EXPECT_EQ(block.cleartext_headers.at(0).value, QString("SHA256")); +} + +// -- whether there is anything to show ---------------------------------------- + +TEST(PGPInspectModelTest, ADocumentWithPacketsHasStructure) { + EXPECT_TRUE(PGPInspectHasStructure(ParsePGPInspectDocument(kDocument))); +} + +TEST(PGPInspectModelTest, PlainTextHasNoStructure) { + // The menu entry is greyed out on exactly this: a valid document saying the + // input is not OpenPGP. Opening a window to be told nothing is worse than + // an entry that cannot be clicked. + EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument( + R"({"format":"binary","size":9,"blocks":[],"errors":["not OpenPGP data"]})"))); +} + +TEST(PGPInspectModelTest, AnUnreadableDocumentHasNoStructure) { + EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument("not json"))); + EXPECT_FALSE(PGPInspectHasStructure({})); +} + +TEST(PGPInspectModelTest, ABlockWithNoPacketsHasNoStructure) { + // An armor envelope whose body decoded to nothing: the header parsed, so + // the document is valid, but there is still no packet tree to render. + EXPECT_FALSE(PGPInspectHasStructure(ParsePGPInspectDocument( + R"({"format":"armored","blocks":[{"kind":"armored","packets":[]}]})"))); +} + +TEST(PGPInspectModelTest, ANestedPacketAloneIsStructureEnough) { + EXPECT_TRUE(PGPInspectHasStructure(ParsePGPInspectDocument( + R"({"blocks":[{"packets":[{"tag":8,"children":[{"tag":2}]}]}]})"))); +} + +// -- the summaries the tree shows --------------------------------------------- + +TEST(PGPInspectModelTest, APacketSummaryNamesTagVersionOffsetAndSize) { + const auto& packet = + ParsePGPInspectDocument(kDocument).blocks.at(0).packets.at(0).children.at( + 0); + const auto summary = PGPInspectPacketSummary(packet, 1); + + EXPECT_TRUE(summary.contains("#1")); + EXPECT_TRUE(summary.contains("Signature")); + EXPECT_TRUE(summary.contains("tag 2")); + EXPECT_TRUE(summary.contains("v4")); + EXPECT_TRUE(summary.contains("@0x0000")); + // Header plus body: the bytes this packet occupies on the wire. + EXPECT_TRUE(summary.contains("103")); +} + +TEST(PGPInspectModelTest, ASummaryFlagsANonFixedLength) { + PGPInspectPacket packet; + packet.tag = 11; + packet.tag_name = "Literal Data"; + packet.length_type = "partial"; + + EXPECT_TRUE(PGPInspectPacketSummary(packet, 2).contains("partial")); +} + +TEST(PGPInspectModelTest, ASummaryOfANamelessPacketStillReads) { + PGPInspectPacket packet; + packet.tag = 60; + + const auto summary = PGPInspectPacketSummary(packet, 1); + EXPECT_TRUE(summary.contains("Unknown")); + EXPECT_TRUE(summary.contains("tag 60")); +} + +TEST(PGPInspectModelTest, ABlockSummaryDistinguishesTheThreeKinds) { + const auto armored = ParsePGPInspectDocument(kDocument).blocks.at(0); + EXPECT_TRUE(PGPInspectBlockSummary(armored, 1).contains("PGP SIGNATURE")); + + PGPInspectBlock cleartext; + cleartext.kind = "cleartext"; + EXPECT_TRUE(PGPInspectBlockSummary(cleartext, 1).contains("cleartext")); + + PGPInspectBlock binary; + binary.kind = "binary"; + EXPECT_TRUE(PGPInspectBlockSummary(binary, 1).contains("binary")); +} |
