14 void parseMultiKeyPublic();
15 void parseMultiKeyPublic_data();
16 void parseSecretKeys();
17 void parseSecretKeys_data();
18 void parseSingleKey();
19 void parseSingleKey_data();
20 void parseKeyRollover();
21 void parseKeyRollover_data();
22 void classifyRecordTypes();
23 void classifyRecordEmpty();
24 void handlePubSecEmptyFields();
25 void handlePubSecShortList();
26 void handleFprEdgeCases();
27 void classifyRecordWithConstQString();
28 void parseGpgColonOutputWithGrpRecord();
29 void parseGpgColonOutputUnknownRecordTypes();
30 void parseGpgColonOutputAllPublicRecordTypes();
33void tst_gpgkeystate::parseMultiKeyPublic() {
34 QFETCH(QString, input);
35 QFETCH(
int, expectedCount);
37 const bool includeSecretKeys =
false;
40 QVERIFY2(result.size() == expectedCount,
41 qPrintable(QString(
"Expected %1 keys, got %2")
43 .arg(result.size())));
45 for (
const UserInfo &user : result) {
46 QVERIFY2(!user.have_secret,
47 "Public keys should not have secret capability");
50 if (expectedCount > 0) {
51 QVERIFY2(!result.at(0).key_id.isEmpty(),
"First key should have key_id");
52 QVERIFY2(!result.at(0).name.isEmpty(),
"First key should have name");
54 if (expectedCount > 1) {
55 QVERIFY2(!result.at(1).key_id.isEmpty(),
"Second key should have key_id");
56 QVERIFY2(!result.at(1).name.isEmpty(),
"Second key should have name");
60void tst_gpgkeystate::parseMultiKeyPublic_data() {
61 QTest::addColumn<QString>(
"input");
62 QTest::addColumn<int>(
"expectedCount");
64 QString input = R
"(tru::1:1775005973:0:3:1:5
65pub:u:4096:1:31850CF72D9CDDE9:1774947438:::u:::escarESCA::::::23::0:
66fpr:::::::::13A47CCE2B3DA3AC340A274A31850CF72D9CDDE9:
67uid:u::::1774947438::CBF23008234AA5F88824CE76140F482FAE34923E::Anne Jan Brouwer <henk@annejan.com>::::::::::0:
68sub:u:4096:1:6DF67C6BAD8383CB:1774947438::::::esa::::::23:
69pub:f:4096:1:693A0AF3FA364E76:1775005968:::f:::escarESCA::::::23::0:
70fpr:::::::::4EF2550F79F4E9E68B09F71D693A0AF3FA364E76:
71uid:f::::1775005968::8AA011711F27F6E08DF71653718C299A13B323A0::Harrie de Bot <harrie@annejan.com>::::::::::0:)";
73 QTest::newRow("two-public-keys") << input << 2;
76void tst_gpgkeystate::parseSecretKeys() {
77 QFETCH(QString, input);
78 QFETCH(
int, expectedCount);
79 QFETCH(
bool, expectHaveSecret);
81 const bool includeSecretKeys =
true;
84 QVERIFY2(result.size() == expectedCount,
85 qPrintable(QString(
"Expected %1 keys, got %2")
87 .arg(result.size())));
89 if (expectedCount > 0) {
90 QVERIFY(!result.isEmpty());
91 for (
int i = 0; i < result.size(); ++i) {
92 const UserInfo &user = result.at(i);
95 qPrintable(QString(
"Key at index %1 has have_secret=%2, expected %3")
98 .arg(expectHaveSecret)));
103void tst_gpgkeystate::parseSecretKeys_data() {
104 QTest::addColumn<QString>(
"input");
105 QTest::addColumn<int>(
"expectedCount");
106 QTest::addColumn<bool>(
"expectHaveSecret");
109 R
"(sec:u:4096:1:31850CF72D9CDDE9:1774947438:::u:::escarESCA:::+:::23::0:
110fpr:::::::::13A47CCE2B3DA3AC340A274A31850CF72D9CDDE9:
111uid:u::::1774947438::CBF23008234AA5F88824CE76140F482FAE34923E::Anne Jan Brouwer <henk@annejan.com>::::::::::0:
112ssb:u:4096:1:6DF67C6BAD8383CB:1774947438::::::esa:::+:::23:)";
114 QTest::newRow("single-secret-key") << input << 1 <<
true;
117void tst_gpgkeystate::parseSingleKey() {
118 QFETCH(QString, input);
119 QFETCH(
int, expectedCount);
120 QFETCH(QString, expectedKeyId);
122 const bool includeSecretKeys =
false;
124 QVERIFY2(result.size() == expectedCount,
125 qPrintable(QString(
"Expected %1 keys, got %2")
127 .arg(result.size())));
129 if (!result.isEmpty()) {
130 QVERIFY2(!result.first().key_id.isEmpty(),
131 "Parsed key should have a key_id");
132 if (!expectedKeyId.isEmpty()) {
133 QVERIFY2(result.first().key_id == expectedKeyId,
134 qPrintable(QString(
"Expected key_id %1, got %2")
136 .arg(result.first().key_id)));
141void tst_gpgkeystate::parseSingleKey_data() {
142 QTest::addColumn<QString>(
"input");
143 QTest::addColumn<int>(
"expectedCount");
144 QTest::addColumn<QString>(
"expectedKeyId");
146 QTest::newRow(
"pub-only")
147 <<
"pub:u:4096:1:ABC123:1774947438:::u::::::23::0:" << 1 <<
"ABC123";
148 QTest::newRow(
"pub-with-fpr") <<
"pub:u:4096:1:ABC123:1774947438:::u::::::23:"
149 ":0:\nfpr:::::::::FINGERPRINT123456789:"
153void tst_gpgkeystate::parseKeyRollover() {
154 QFETCH(QString, input);
155 QFETCH(
int, expectedCount);
157 const bool includeSecretKeys =
false;
159 QVERIFY2(result.size() == expectedCount,
160 qPrintable(QString(
"Expected %1 keys, got %2")
162 .arg(result.size())));
164 auto containsKeyId = [&](
const QString &keyId) {
166 result.cbegin(), result.cend(),
167 [&](
const UserInfo &user) { return user.key_id == keyId; });
170 QVERIFY2(containsKeyId(
"AAA111"),
"Expected AAA111 key to be parsed");
171 QVERIFY2(containsKeyId(
"BBB222"),
"Expected BBB222 key to be parsed");
172 QVERIFY2(containsKeyId(
"CCC333"),
"Expected CCC333 key to be parsed");
175void tst_gpgkeystate::parseKeyRollover_data() {
176 QTest::addColumn<QString>(
"input");
177 QTest::addColumn<int>(
"expectedCount");
179 QString input = R
"(pub:u:4096:1:AAA111:1774947438:::u::::
180fpr:::::::::AAA111FINGERPRINT:
181uid:u::::1774947438::NAME1::user1@test.com:::::::0:
182pub:u:4096:1:BBB222:1774947438:::u::::
183fpr:::::::::BBB222FINGERPRINT:
184uid:u::::1774947438::NAME2::user2@test.com:::::::0:
185pub:u:4096:1:CCC333:1774947438:::u::::
186fpr:::::::::CCC333FINGERPRINT:
187uid:u::::1774947438::NAME3::user3@test.com:::::::0:)";
189 QTest::newRow("three-keys-rollover") << input << 3;
192void tst_gpgkeystate::classifyRecordTypes() {
194 "Should classify pub record");
196 "Should classify sec record");
198 "Should classify uid record");
200 "Should classify fpr record");
202 "Should classify sub record");
204 "Should classify ssb record");
206 "Should classify grp record");
208 "Should classify unknown record types as Unknown");
211void tst_gpgkeystate::classifyRecordEmpty() {
213 "Should classify empty as Unknown");
215 "Should be case-sensitive");
217 "Should not match partial");
220void tst_gpgkeystate::handlePubSecEmptyFields() {
236 QVERIFY2(user.
key_id ==
"keyId00001",
"Should parse key_id");
237 QVERIFY2(user.
name ==
"Test User",
"Should parse name");
238 QVERIFY2(user.
validity ==
'-',
"Empty validity should be dash");
239 QVERIFY2(!user.
created.isValid(),
"Empty created should be invalid");
240 QVERIFY2(!user.
expiry.isValid(),
"Empty expiry should be invalid");
241 QVERIFY2(!user.
have_secret,
"Should not have secret");
244void tst_gpgkeystate::handlePubSecShortList() {
245 QStringList shortProps;
246 shortProps.append(
"pub");
247 shortProps.append(
"");
248 shortProps.append(
"4096");
249 shortProps.append(
"1");
250 shortProps.append(
"");
252 auto verifyShortListIgnored = [](
const UserInfo &u,
const char *ctx) {
253 QVERIFY2(u.
key_id.isEmpty(),
"Short list should be ignored");
254 QVERIFY2(u.
name.isEmpty(),
"Short list ignored: name empty");
255 QVERIFY2(u.
validity ==
'-',
"Short list ignored: default validity");
256 QVERIFY2(!u.
created.isValid(),
"Short list ignored: created invalid");
257 QVERIFY2(!u.
expiry.isValid(),
"Short list ignored: expiry invalid");
263 verifyShortListIgnored(publicUser,
264 "Short list ignored: no secret for public");
268 verifyShortListIgnored(secretUser,
269 "Short list ignored: no secret for secret input");
272void tst_gpgkeystate::handleFprEdgeCases() {
276 QStringList emptyProps;
277 for (
int i = 0; i < 10; ++i)
278 emptyProps.append(
"");
280 QVERIFY2(user.
key_id ==
"id123",
"Empty fpr should not change key_id");
282 QStringList nonMatchingProps;
283 for (
int i = 0; i < 10; ++i)
284 nonMatchingProps.append(
"");
285 nonMatchingProps[9] =
"otherFingerprint";
287 QVERIFY2(user.
key_id ==
"id123",
"Non-matching fpr should not change key_id");
290 QStringList matchingProps;
291 for (
int i = 0; i < 10; ++i)
292 matchingProps.append(
"");
293 matchingProps[9] =
"full fingerprint id456";
295 QVERIFY2(user.
key_id ==
"full fingerprint id456",
296 "fpr ending with key_id should update to full fingerprint");
305void tst_gpgkeystate::classifyRecordWithConstQString() {
308 const QString pubTag = QStringLiteral(
"pub");
311 const QString secTag = QStringLiteral(
"sec");
314 const QString uidTag = QStringLiteral(
"uid");
317 const QString fprTag = QStringLiteral(
"fpr");
320 const QString subTag = QStringLiteral(
"sub");
323 const QString ssbTag = QStringLiteral(
"ssb");
326 const QString grpTag = QStringLiteral(
"grp");
329 const QString unknownTag = QStringLiteral(
"tru");
333void tst_gpgkeystate::parseGpgColonOutputWithGrpRecord() {
337 const QString input =
338 QStringLiteral(
"pub:u:4096:1:AAABBBCCC:1774947438:::u::::\n"
339 "grp:::::::::GROUPKEYID:\n"
340 "uid:u::::1774947438::HASH::Alice <alice@test.org>::::\n");
343 QVERIFY2(result.size() == 1,
344 "grp record should be ignored; only one key expected");
345 QVERIFY2(result.first().key_id == QStringLiteral(
"AAABBBCCC"),
346 "key_id should be parsed correctly alongside grp record");
349void tst_gpgkeystate::parseGpgColonOutputUnknownRecordTypes() {
353 const QString input =
354 QStringLiteral(
"tru::1:9999999999:0:3:1:5\n"
355 "pub:f:4096:1:DEADBEEF01:1774947438:::f::::\n"
356 "rvk:::::::::REVOKER_FINGERPRINT:\n"
357 "fpr:::::::::DEADBEEF01FINGERPRINT:\n"
358 "uid:f::::1774947438::H::Bob <bob@test.org>::::\n");
361 QVERIFY2(result.size() == 1,
362 "tru/rvk records should be ignored; one key expected");
363 QVERIFY2(result.first().key_id == QStringLiteral(
"DEADBEEF01"),
364 "key_id should be parsed despite surrounding unknown records");
367void tst_gpgkeystate::parseGpgColonOutputAllPublicRecordTypes() {
371 const QString input =
372 QStringLiteral(
"pub:u:4096:1:MAINKEY001:1774947438:::u::::\n"
373 "fpr:::::::::MAINKEY001FINGERPRINT:\n"
374 "uid:u::::1774947438::HASH::Carol <carol@test.org>::::\n"
375 "sub:u:4096:1:SUBKEY001:1774947438::::::esa:::\n"
376 "ssb:u:4096:1:SUBKEY002:1774947438::::::esa:::\n");
380 QVERIFY2(result.size() == 1,
381 "sub/ssb records should not create additional UserInfo entries");
382 QVERIFY2(result.first().key_id == QStringLiteral(
"MAINKEY001"),
383 "key_id should reflect the pub record");
384 QVERIFY2(!result.first().name.isEmpty(),
"UID name should be populated");
388#include "tst_gpgkeystate.moc"
void handlePubSecRecord(const QStringList &props, bool secret, UserInfo ¤t_user)
Handle a pub or sec record in GPG colon output.
auto parseGpgColonOutput(const QString &output, bool secret) -> QList< UserInfo >
Parse GPG –with-colons output into a list of UserInfo.
auto classifyRecord(const QString &record_type) -> GpgRecordType
Classify a GPG colon output record type.
void handleFprRecord(const QStringList &props, UserInfo ¤t_user)
Handle an fpr (fingerprint) record in GPG colon output.
Stores key info lines including validity, creation date and more.
bool have_secret
UserInfo::have_secret whether secret key is available (can decrypt with this key).
QString key_id
UserInfo::key_id hexadecimal representation of the GnuPG key identifier.
QDateTime created
UserInfo::created date/time when key was created.
QString name
UserInfo::name GPG user ID / full name.
char validity
UserInfo::validity GnuPG representation of validity http://git.gnupg.org/cgi-bin/gitweb....
QDateTime expiry
UserInfo::expiry date/time when key expires.