Skip to content

Commit 9e6e4d0

Browse files
committed
quic: complete the QUIC tests
Signed-off-by: James M Snell <[email protected]> Assisted-by: Opencode:Opus 4.6
1 parent 47f34f7 commit 9e6e4d0

219 files changed

Lines changed: 15382 additions & 333 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

test/cctest/test_dataqueue.cc

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -495,25 +495,10 @@ TEST(DataQueue, NonIdempotentDataQueue) {
495495
CHECK(!waitingForPull);
496496
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
497497

498-
// We can read the expected data from reader1. Because the entries are
499-
// InMemoryEntry instances, reads will be fully synchronous here.
498+
// The next read produces buffer2. When the first entry's reader returns
499+
// EOS, the NonIdempotentDataQueueReader immediately pulls from the next
500+
// entry (recursive Pull), so the transition is seamless.
500501
waitingForPull = true;
501-
502-
status = reader->Pull(
503-
[&](int status, const DataQueue::Vec* vecs, size_t count, auto done) {
504-
waitingForPull = false;
505-
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
506-
CHECK_EQ(count, 0);
507-
},
508-
node::bob::OPTIONS_SYNC,
509-
nullptr,
510-
0,
511-
node::bob::kMaxCountHint);
512-
513-
CHECK(!waitingForPull);
514-
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
515-
516-
// The next read produces buffer2, and should be the end.
517502
status = reader->Pull(
518503
[&](int status, const DataQueue::Vec* vecs, size_t count, auto done) {
519504
waitingForPull = false;
@@ -628,6 +613,9 @@ TEST(DataQueue, DataQueueEntry) {
628613
CHECK(!pullIsPending);
629614
CHECK_EQ(status, node::bob::STATUS_CONTINUE);
630615

616+
// Cap the queue so the reader can reach EOS after draining all entries.
617+
data_queue2->cap();
618+
631619
// Read to completion...
632620
while (status != node::bob::STATUS_EOS) {
633621
status = reader->Pull(

test/cctest/test_quic_tokens.cc

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ TEST(StatelessResetToken, Basic) {
5656

5757
CHECK_EQ(token, token2);
5858

59-
// Let's pretend out secret is also a token just for the sake
59+
// Let's pretend our secret is also a token just for the sake
6060
// of the test. That's ok because they're the same length.
6161
StatelessResetToken token3(secret);
6262

@@ -85,6 +85,83 @@ TEST(StatelessResetToken, Basic) {
8585
CHECK_EQ(found->second, token);
8686
}
8787

88+
TEST(StatelessResetToken, Ngtcp2StructIntegration) {
89+
uint8_t secret[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6};
90+
uint8_t cid_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
91+
ngtcp2_cid cid_;
92+
ngtcp2_cid_init(&cid_, cid_data, 10);
93+
TokenSecret fixed_secret(secret);
94+
CID cid(cid_);
95+
96+
// Owning token — generated into the inherited ngtcp2_stateless_reset_token
97+
StatelessResetToken owning(fixed_secret, cid);
98+
CHECK(owning);
99+
100+
// The ngtcp2_stateless_reset_token* conversion operator should return
101+
// a valid pointer to the token data.
102+
const ngtcp2_stateless_reset_token* as_struct = owning;
103+
CHECK_NE(as_struct, nullptr);
104+
// The struct's data should match the uint8_t* conversion.
105+
const uint8_t* as_bytes = owning;
106+
CHECK_EQ(
107+
memcmp(
108+
as_struct->data, as_bytes, StatelessResetToken::kStatelessTokenLen),
109+
0);
110+
111+
// Non-owning from const ngtcp2_stateless_reset_token* — wraps an
112+
// existing struct without copying.
113+
StatelessResetToken from_struct(as_struct);
114+
CHECK(from_struct);
115+
CHECK_EQ(from_struct, owning);
116+
// The pointer should be the same (non-owning wraps, doesn't copy).
117+
const ngtcp2_stateless_reset_token* from_struct_ptr = from_struct;
118+
CHECK_EQ(from_struct_ptr, as_struct);
119+
120+
// Owning into external ngtcp2_stateless_reset_token — generates the
121+
// token into a caller-provided struct.
122+
ngtcp2_stateless_reset_token external_struct{};
123+
StatelessResetToken into_struct(&external_struct, fixed_secret, cid);
124+
CHECK(into_struct);
125+
CHECK_EQ(into_struct, owning);
126+
// The external struct should now contain the generated token.
127+
CHECK_EQ(memcmp(external_struct.data,
128+
as_bytes,
129+
StatelessResetToken::kStatelessTokenLen),
130+
0);
131+
// The conversion operator should return a pointer to the external struct.
132+
const ngtcp2_stateless_reset_token* into_struct_ptr = into_struct;
133+
CHECK_EQ(into_struct_ptr, &external_struct);
134+
135+
// Copy of an owning token should itself be owning (independent copy).
136+
StatelessResetToken copy_of_owning = owning;
137+
CHECK_EQ(copy_of_owning, owning);
138+
const ngtcp2_stateless_reset_token* copy_ptr = copy_of_owning;
139+
// Should NOT point to the same memory as the original.
140+
CHECK_NE(copy_ptr, as_struct);
141+
// But data should match.
142+
CHECK_EQ(
143+
memcmp(copy_ptr->data, as_bytes, StatelessResetToken::kStatelessTokenLen),
144+
0);
145+
146+
// Copy of a non-owning token should become owning (copies data).
147+
StatelessResetToken copy_of_non_owning = from_struct;
148+
CHECK_EQ(copy_of_non_owning, from_struct);
149+
const ngtcp2_stateless_reset_token* copy_no_ptr = copy_of_non_owning;
150+
// Should NOT point to the original non-owning source.
151+
CHECK_NE(copy_no_ptr, from_struct_ptr);
152+
153+
// kInvalid conversions.
154+
const ngtcp2_stateless_reset_token* invalid_ptr =
155+
StatelessResetToken::kInvalid;
156+
CHECK_EQ(invalid_ptr, nullptr);
157+
const uint8_t* invalid_bytes = StatelessResetToken::kInvalid;
158+
// When ptr_ is null, falls back to inherited data (zeroed).
159+
uint8_t zeroed[StatelessResetToken::kStatelessTokenLen]{};
160+
CHECK_EQ(
161+
memcmp(invalid_bytes, zeroed, StatelessResetToken::kStatelessTokenLen),
162+
0);
163+
}
164+
88165
TEST(RetryToken, Basic) {
89166
auto& random = CID::Factory::random();
90167
TokenSecret secret;

test/cctest/test_sockaddr.cc

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
#include "node_sockaddr-inl.h"
21
#include "gtest/gtest.h"
2+
#include "node_sockaddr-inl.h"
33

44
using node::SocketAddress;
55
using node::SocketAddressBlockList;
@@ -43,6 +43,85 @@ TEST(SocketAddress, SocketAddress) {
4343
CHECK_EQ(map[addr], 2);
4444
}
4545

46+
TEST(SocketAddress, IpHashAndIpEqual) {
47+
sockaddr_storage s1, s2, s3, s4;
48+
// Same IP, different ports.
49+
SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 443, &s1);
50+
SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 8080, &s2);
51+
// Different IP.
52+
SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 443, &s3);
53+
54+
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&s1));
55+
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&s2));
56+
SocketAddress addr3(reinterpret_cast<const sockaddr*>(&s3));
57+
58+
SocketAddress::IpHash ip_hash;
59+
SocketAddress::IpEqual ip_equal;
60+
61+
// Same IP, different port: should hash equal and compare equal.
62+
CHECK_EQ(ip_hash(addr1), ip_hash(addr2));
63+
CHECK(ip_equal(addr1, addr2));
64+
65+
// Different IP: should not compare equal.
66+
CHECK(!ip_equal(addr1, addr3));
67+
68+
// Full Hash (includes port) should differ for same IP, different port.
69+
CHECK_NE(SocketAddress::Hash()(addr1), SocketAddress::Hash()(addr2));
70+
71+
// IpMap should treat same-IP-different-port as the same key.
72+
SocketAddress::IpMap<uint16_t> map;
73+
map[addr1] = 1;
74+
map[addr2]++; // Same IP as addr1, should increment the same entry.
75+
CHECK_EQ(map[addr1], 2);
76+
CHECK_EQ(map.size(), 1);
77+
78+
map[addr3] = 10;
79+
CHECK_EQ(map.size(), 2);
80+
CHECK_EQ(map[addr3], 10);
81+
}
82+
83+
TEST(SocketAddress, IpHashIPv6) {
84+
sockaddr_storage s1, s2, s3;
85+
SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &s1);
86+
SocketAddress::ToSockAddr(AF_INET6, "::1", 8080, &s2);
87+
SocketAddress::ToSockAddr(AF_INET6, "::2", 443, &s3);
88+
89+
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&s1));
90+
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&s2));
91+
SocketAddress addr3(reinterpret_cast<const sockaddr*>(&s3));
92+
93+
SocketAddress::IpHash ip_hash;
94+
SocketAddress::IpEqual ip_equal;
95+
96+
// Same IPv6, different port: equal.
97+
CHECK_EQ(ip_hash(addr1), ip_hash(addr2));
98+
CHECK(ip_equal(addr1, addr2));
99+
100+
// Different IPv6: not equal.
101+
CHECK(!ip_equal(addr1, addr3));
102+
103+
// IpMap with IPv6 keys.
104+
SocketAddress::IpMap<uint16_t> map;
105+
map[addr1] = 5;
106+
map[addr2]++;
107+
CHECK_EQ(map[addr1], 6);
108+
CHECK_EQ(map.size(), 1);
109+
}
110+
111+
TEST(SocketAddress, IpEqualCrossFamily) {
112+
sockaddr_storage s1, s2;
113+
SocketAddress::ToSockAddr(AF_INET, "127.0.0.1", 443, &s1);
114+
SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &s2);
115+
116+
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&s1));
117+
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&s2));
118+
119+
SocketAddress::IpEqual ip_equal;
120+
121+
// Different address families should never be equal.
122+
CHECK(!ip_equal(addr1, addr2));
123+
}
124+
46125
TEST(SocketAddress, SocketAddressIPv6) {
47126
sockaddr_storage storage;
48127
SocketAddress::ToSockAddr(AF_INET6, "::1", 443, &storage);
@@ -85,7 +164,6 @@ TEST(SocketAddressLRU, SocketAddressLRU) {
85164
SocketAddress::ToSockAddr(AF_INET, "123.123.123.125", 443, &storage[2]);
86165
SocketAddress::ToSockAddr(AF_INET, "123.123.123.123", 443, &storage[3]);
87166

88-
89167
SocketAddress addr1(reinterpret_cast<const sockaddr*>(&storage[0]));
90168
SocketAddress addr2(reinterpret_cast<const sockaddr*>(&storage[1]));
91169
SocketAddress addr3(reinterpret_cast<const sockaddr*>(&storage[2]));
@@ -197,12 +275,10 @@ TEST(SocketAddressBlockList, Simple) {
197275
sockaddr_storage storage[2];
198276
SocketAddress::ToSockAddr(AF_INET, "10.0.0.1", 0, &storage[0]);
199277
SocketAddress::ToSockAddr(AF_INET, "10.0.0.2", 0, &storage[1]);
200-
std::shared_ptr<SocketAddress> addr1 =
201-
std::make_shared<SocketAddress>(
202-
reinterpret_cast<const sockaddr*>(&storage[0]));
203-
std::shared_ptr<SocketAddress> addr2 =
204-
std::make_shared<SocketAddress>(
205-
reinterpret_cast<const sockaddr*>(&storage[1]));
278+
std::shared_ptr<SocketAddress> addr1 = std::make_shared<SocketAddress>(
279+
reinterpret_cast<const sockaddr*>(&storage[0]));
280+
std::shared_ptr<SocketAddress> addr2 = std::make_shared<SocketAddress>(
281+
reinterpret_cast<const sockaddr*>(&storage[1]));
206282

207283
bl.AddSocketAddress(addr1);
208284
bl.AddSocketAddress(addr2);

test/common/quic.mjs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Shared helpers for QUIC tests.
2+
//
3+
// Usage:
4+
// import { key, cert, listen, connect } from '../common/quic.mjs';
5+
//
6+
// Provides pre-loaded TLS credentials and thin wrappers around node:quic
7+
// listen/connect that apply default options suitable for most tests.
8+
9+
import * as fixtures from '../common/fixtures.mjs';
10+
11+
const { createPrivateKey } = await import('node:crypto');
12+
const quic = await import('node:quic');
13+
14+
// Pre-loaded TLS credentials from the standard agent1 fixture pair.
15+
const key = createPrivateKey(fixtures.readKey('agent1-key.pem'));
16+
const cert = fixtures.readKey('agent1-cert.pem');
17+
18+
/**
19+
* Start a QUIC server with sensible test defaults.
20+
* @param {Function} callback The session callback (receives QuicSession).
21+
* @param {object} [options] Options forwarded to quic.listen(). The
22+
* following defaults are applied when not specified:
23+
* - sni: { '*': { keys: [key], certs: [cert] } }
24+
* - alpn: ['quic-test']
25+
* @returns {Promise<QuicEndpoint>}
26+
*/
27+
async function listen(callback, options = {}) {
28+
const {
29+
sni = { '*': { keys: [key], certs: [cert] } },
30+
alpn = ['quic-test'],
31+
...rest
32+
} = options;
33+
return quic.listen(callback, { sni, alpn, ...rest });
34+
}
35+
36+
/**
37+
* Connect a QUIC client with sensible test defaults.
38+
* @param {SocketAddress|string} address The server address.
39+
* @param {object} [options] Options forwarded to quic.connect(). The
40+
* following defaults are applied when not specified:
41+
* - alpn: 'quic-test'
42+
* @returns {Promise<QuicSession>}
43+
*/
44+
async function connect(address, options = {}) {
45+
const {
46+
alpn = 'quic-test',
47+
...rest
48+
} = options;
49+
return quic.connect(address, { alpn, ...rest });
50+
}
51+
52+
export {
53+
key,
54+
cert,
55+
listen,
56+
connect,
57+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-----BEGIN X509 CRL-----
2+
MIIB/jCB5wIBATANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQGEwJVUzELMAkGA1UE
3+
CAwCQ0ExCzAJBgNVBAcMAlNGMQ8wDQYDVQQKDAZKb3llbnQxEDAOBgNVBAsMB05v
4+
ZGUuanMxDDAKBgNVBAMMA2NhMjEgMB4GCSqGSIb3DQEJARYRcnlAdGlueWNsb3Vk
5+
cy5vcmcXDTI2MDQxNjA0MDM1MloYDzIwNTMwOTAxMDQwMzUyWjAnMCUCFHtnB1Iw
6+
05rTKjL+Xc+x+pXi6jGdFw0yNjA0MTYwNDAzNTJaoA4wDDAKBgNVHRQEAwIBATAN
7+
BgkqhkiG9w0BAQ0FAAOCAQEAS7PnQxPHv+VXvmCOcTQOYWns16+G5cmaY8/fYjwM
8+
6zOQPTItJTH+S2EJ3JvqES3Xm3KH+2Qh/8gAiiGNL9zdBpuNcJyUlJpIPuvWPd0P
9+
Bup7u2YEvc9NjuP8thslf267A8tieFf4mF+AO1lvFp+CGoyRSwtNGOWCMkFDGgGn
10+
ZOVXw5Q782PhUwThozGjR40zDkNjW/uFPJjMkz/RZFEmWshGf9t3VzahRs8PUApr
11+
XTdatufBUPrWiTWyQAuME50ajzq/tfuj2kokqfOvy1mkoNwtySVxKSlwGjejd5Xj
12+
yV/v4a5FDjXw4AwqEe+Cul9J2eyBb1jHkc+R9rutHTKEZA==
13+
-----END X509 CRL-----
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Flags: --experimental-quic --no-warnings
2+
3+
// Test: validateAddress triggers Retry flow.
4+
// When the server endpoint has validateAddress: true, it should send
5+
// a Retry packet before accepting the connection. The handshake still
6+
// completes successfully.
7+
8+
import { hasQuic, skip, mustCall } from '../common/index.mjs';
9+
import assert from 'node:assert';
10+
import * as fixtures from '../common/fixtures.mjs';
11+
12+
const { strictEqual } = assert;
13+
const { readKey } = fixtures;
14+
15+
if (!hasQuic) {
16+
skip('QUIC is not enabled');
17+
}
18+
19+
const { listen, connect, QuicEndpoint } = await import('node:quic');
20+
const { createPrivateKey } = await import('node:crypto');
21+
22+
const key = createPrivateKey(readKey('agent1-key.pem'));
23+
const cert = readKey('agent1-cert.pem');
24+
25+
const endpoint = new QuicEndpoint({ validateAddress: true });
26+
27+
const serverEndpoint = await listen(mustCall(async (serverSession) => {
28+
const info = await serverSession.opened;
29+
// The handshake should complete despite the Retry flow.
30+
strictEqual(info.protocol, 'quic-test');
31+
serverSession.close();
32+
}), {
33+
endpoint,
34+
sni: { '*': { keys: [key], certs: [cert] } },
35+
alpn: ['quic-test'],
36+
});
37+
38+
const clientSession = await connect(serverEndpoint.address, {
39+
alpn: 'quic-test',
40+
servername: 'localhost',
41+
});
42+
43+
const info = await clientSession.opened;
44+
strictEqual(info.protocol, 'quic-test');
45+
46+
// The serverEndpoint must be closed after we wait for the clientSession to close.
47+
await clientSession.closed;
48+
await serverEndpoint.close();

0 commit comments

Comments
 (0)