Skip to content

Commit 94543a4

Browse files
committed
add tests for zt_jwt_parse function to validate error handling and correct parsing
copilot/claude generated
1 parent ded9bd4 commit 94543a4

File tree

1 file changed

+87
-0
lines changed

1 file changed

+87
-0
lines changed

tests/test_jwt.cpp

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "catch2/catch_test_macros.hpp"
1616
#include "catch2/matchers/catch_matchers_string.hpp"
17+
#include <catch2/generators/catch_generators.hpp>
1718
#include <cstring>
1819
#include <zt_internal.h>
1920
#include "internal_model.h"
@@ -94,6 +95,92 @@ TEST_CASE("zt_jwt_parse","[model]") {
9495
zt_jwt_drop(&jwt_struct);
9596
}
9697

98+
TEST_CASE("zt_jwt_parse negative", "[model]") {
99+
auto tc = GENERATE(table<const char*, const char*>({
100+
// structure errors
101+
{"empty string", ""},
102+
{"no dots", "eyJhbGciOiJSUzI1NiJ9"},
103+
{"only one dot", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0"},
104+
{"dots only", "..."},
105+
{"empty segments", ".."},
106+
107+
// header decode/parse errors
108+
{"invalid base64 header", "!!!invalid!!!.cGF5bG9hZA.c2ln"},
109+
{"header not json", "bm90IGpzb24.cGF5bG9hZA.c2ln"}, // "not json"
110+
111+
// payload decode/parse errors
112+
{"invalid base64 payload", "eyJhbGciOiJSUzI1NiJ9.!!!invalid!!!.c2ln"},
113+
{"payload not json", "eyJhbGciOiJSUzI1NiJ9.bm90IGpzb24.c2ln"}, // "not json"
114+
{"payload is json array", "eyJhbGciOiJSUzI1NiJ9.WzEsIDJd.c2ln"}, // [1, 2]
115+
{"empty json payload", "eyJhbGciOiJSUzI1NiJ9.e30.c2ln"}, // {}
116+
117+
// iss claim errors
118+
{"missing iss claim", "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.c2ln"}, // {"sub":"test"}
119+
{"iss not a string", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOjEyM30.c2ln"}, // {"iss":123}
120+
{"iss is null", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiBudWxsfQ.c2ln"}, // {"iss": null}
121+
{"iss is boolean", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiB0cnVlfQ.c2ln"}, // {"iss": true}
122+
{"iss is array", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiBbImEiXX0.c2ln"}, // {"iss": ["a"]}
123+
{"iss is object", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiB7fX0.c2ln"}, // {"iss": {}}
124+
125+
// exp claim errors
126+
{"exp not an int", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoibm90YW5pbnQifQ.c2ln"}, // {"iss":"test","exp":"notanint"}
127+
{"exp is boolean", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiAidGVzdCIsICJleHAiOiB0cnVlfQ.c2ln"}, // {"iss":"test","exp":true}
128+
{"exp is float", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiAidGVzdCIsICJleHAiOiAxLjV9.c2ln"}, // {"iss":"test","exp":1.5}
129+
}));
130+
131+
DYNAMIC_SECTION(std::get<0>(tc)) {
132+
zt_jwt jwt_struct{};
133+
int rc = zt_jwt_parse(std::get<1>(tc), &jwt_struct);
134+
CHECK(rc != 0);
135+
}
136+
}
137+
138+
TEST_CASE("zt_jwt_parse valid", "[model]") {
139+
struct valid_case {
140+
const char *label;
141+
const char *input;
142+
const char *expected_iss;
143+
uint64_t expected_exp;
144+
};
145+
auto tc = GENERATE(values<valid_case>({
146+
// {"iss":"https://example.com","exp":1704661695}
147+
{"with exp",
148+
"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiZXhwIjoxNzA0NjYxNjk1fQ.c2ln",
149+
"https://example.com", 1704661695},
150+
// {"iss":"https://example.com"}
151+
{"without exp",
152+
"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIn0.c2ln",
153+
"https://example.com", 0},
154+
// json-c represents JSON null as NULL pointer, parser treats it as absent exp
155+
// {"iss":"test","exp":null}
156+
{"exp is null",
157+
"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiAidGVzdCIsICJleHAiOiBudWxsfQ.c2ln",
158+
"test", 0},
159+
// parser doesn't validate header structure, only that it's valid JSON
160+
// header: [1, 2], payload: {"iss":"test"}
161+
{"header is json array",
162+
"WzEsIDJd.eyJpc3MiOiJ0ZXN0In0.c2ln",
163+
"test", 0},
164+
}));
165+
166+
DYNAMIC_SECTION(tc.label) {
167+
zt_jwt jwt_struct{};
168+
int rc = zt_jwt_parse(tc.input, &jwt_struct);
169+
CHECK(rc == 0);
170+
CHECK_THAT(cstr_str(&jwt_struct.issuer), Catch::Matchers::Equals(tc.expected_iss));
171+
CHECK(jwt_struct.expiration == tc.expected_exp);
172+
zt_jwt_drop(&jwt_struct);
173+
}
174+
}
175+
176+
TEST_CASE("zt_jwt_parse extra dots", "[model]") {
177+
zt_jwt jwt_struct{};
178+
// header.payload.sig.extra — extra segment; verify no crash
179+
int rc = zt_jwt_parse("eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0.c2ln.ZXh0cmE", &jwt_struct);
180+
(void)rc;
181+
zt_jwt_drop(&jwt_struct);
182+
}
183+
97184
TEST_CASE("jwt cred", "[model]") {
98185
ziti_credential_t *cred = nullptr;
99186
int rc = ziti_credential_from_jwt(jwt, &cred);

0 commit comments

Comments
 (0)