1 #include "tests/test.hpp"
2 #include "tests/unit/connector/certs.hpp"
3 #include "tests/unit/connector/mock_server.hpp"
4 #include "tests/unit/connector/connector_utils.hpp"
5
6 #include <cpp-pcp-client/connector/connection.hpp>
7 #include <cpp-pcp-client/connector/client_metadata.hpp>
8 #include <cpp-pcp-client/connector/errors.hpp>
9 #include <cpp-pcp-client/connector/timings.hpp>
10
11 #include <cpp-pcp-client/util/chrono.hpp>
12
13 #include <boost/nowide/iostream.hpp>
14
15 #define LEATHERMAN_LOGGING_NAMESPACE "puppetlabs.cpp_pcp_client.test"
16 #include <leatherman/logging/logging.hpp>
17
18 #include <leatherman/util/timer.hpp>
19
20 #include <memory>
21
22 using namespace PCPClient;
23
24 namespace lth_util = leatherman::util;
25
26 static constexpr uint32_t PONG_LONG_TIMEOUT_MS { 30000 };
27
28 TEST_CASE("Connection::connect errors", "[connection]") {
29 SECTION("throws a connection_processing_error if the broker url is"
30 "not a valid WebSocket url") {
31 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
32 getKeyPath(), 6,
33 PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
34 // NB: the dtor will wait for the 6 ms specified above
35 Connection connection { "foo", c_m };
36
37 REQUIRE_THROWS_AS(connection.connect(),
38 connection_processing_error);
39 }
40 }
41
42 TEST_CASE("Connection timings", "[connection]") {
43 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
44 getKeyPath(), std::string{}, std::string{},
45 leatherman::logging::log_level::error, &boost::nowide::cout,
46 WS_TIMEOUT_MS, PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
47
48 SECTION("can stringify timings") {
49 Connection connection { "wss://localhost:8142/pcp", c_m };
50 REQUIRE_NOTHROW(connection.timings.toString());
51 }
52
53 SECTION("WebSocket timings are retrieved") {
54 auto start = boost::chrono::high_resolution_clock::now();
55 MockServer mock_server;
56 mock_server.go();
57 Connection connection {
58 "wss://localhost:" + std::to_string(mock_server.port()) + "/pcp", c_m };
59 connection.connect(1);
60 auto tot = boost::chrono::duration_cast<ConnectionTimings::Duration_us>(
61 boost::chrono::high_resolution_clock::now() - start);
62 auto duration_zero = ConnectionTimings::Duration_us::zero();
63
64 REQUIRE(duration_zero < connection.timings.getTCPInterval());
65 REQUIRE(duration_zero < connection.timings.getOpeningHandshakeInterval());
66 REQUIRE(duration_zero < connection.timings.getWebSocketInterval());
67 REQUIRE(duration_zero < connection.timings.getOverallConnectionInterval_us());
68 REQUIRE(duration_zero == connection.timings.getClosingHandshakeInterval());
69 REQUIRE(connection.timings.getWebSocketInterval() < tot);
70 }
71 }
72
let_connection_stop(Connection const & connection,int timeout=2)73 static void let_connection_stop(Connection const& connection, int timeout = 2)
74 {
75 lth_util::Timer timer {};
76 while (connection.getConnectionState() == ConnectionState::open
77 && timer.elapsed_seconds() < timeout)
78 Util::this_thread::sleep_for(Util::chrono::milliseconds(1));
79 REQUIRE(connection.getConnectionState() != ConnectionState::open);
80 }
81
wait_for_server_open()82 static void wait_for_server_open()
83 {
84 static Util::chrono::milliseconds pause {20};
85 Util::this_thread::sleep_for(pause);
86 }
87
88 TEST_CASE("Connection::connect", "[connection]") {
89 SECTION("successfully connects, closes, and sets Closing Handshake timings") {
90 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
91 getKeyPath(), WS_TIMEOUT_MS,
92 PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
93
94 MockServer mock_server;
95 bool connected = false;
__anoncab39b980102(websocketpp::connection_hdl hdl) 96 mock_server.set_open_handler([&connected](websocketpp::connection_hdl hdl) {
97 connected = true;
98 });
99 mock_server.go();
100
101 Connection connection {
102 "wss://localhost:" + std::to_string(mock_server.port()) + "/pcp", c_m };
103 connection.connect(10);
104 wait_for_server_open();
105 REQUIRE(connected);
106
107 connection.close();
108 auto duration_zero = ConnectionTimings::Duration_us::zero();
109
110 // Let's wait for the connection to close and retrieve the duration
111 // NB: we can't use let_connection_stop as the connection state
112 // will become `closing`
113 lth_util::Timer timer {};
114
115 while (connection.getConnectionState() != ConnectionState::closed
116 && timer.elapsed_seconds() < 2)
117 Util::this_thread::sleep_for(Util::chrono::milliseconds(1));
118
119 REQUIRE(connection.getConnectionState() == ConnectionState::closed);
120 REQUIRE(duration_zero < connection.timings.getClosingHandshakeInterval());
121 }
122
123 SECTION("successfully connects to failover broker") {
124 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
125 getKeyPath(), WS_TIMEOUT_MS,
126 PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
127
128 MockServer mock_server;
129 bool connected = false;
__anoncab39b980202(websocketpp::connection_hdl hdl) 130 mock_server.set_open_handler([&connected](websocketpp::connection_hdl hdl) {
131 connected = true;
132 });
133 mock_server.go();
134
135 auto port = mock_server.port();
136 Connection connection {
137 std::vector<std::string> { "wss://localhost:" + std::to_string(port+1) + "/pcp",
138 "wss://localhost:" + std::to_string(port) + "/pcp" },
139 c_m };
140 connection.connect(30);
141 wait_for_server_open();
142 REQUIRE(connected);
143
144 connection.close();
145 let_connection_stop(connection);
146 }
147
148 SECTION("successfully connects to failover when primary broker disappears") {
149 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
150 getKeyPath(), WS_TIMEOUT_MS,
151 PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
152
153 bool connected_a = false, connected_b = false, connected_c = false;
154
155 std::unique_ptr<MockServer> mock_server_a(new MockServer());
__anoncab39b980302(websocketpp::connection_hdl hdl) 156 mock_server_a->set_open_handler([&connected_a](websocketpp::connection_hdl hdl) {
157 connected_a = true;
158 });
159
160 std::unique_ptr<MockServer> mock_server_b(new MockServer());
__anoncab39b980402(websocketpp::connection_hdl hdl) 161 mock_server_b->set_open_handler([&connected_b](websocketpp::connection_hdl hdl) {
162 connected_b = true;
163 });
164
165 auto port_a = mock_server_a->port();
166 auto port_b = mock_server_b->port();
167 Connection connection {
168 std::vector<std::string> { "wss://localhost:" + std::to_string(port_a) + "/pcp",
169 "wss://localhost:" + std::to_string(port_b) + "/pcp" },
170 c_m };
171
172 mock_server_a->go();
173 connection.connect(10);
174 wait_for_server_open();
175 REQUIRE(connected_a);
176 mock_server_a.reset();
177 let_connection_stop(connection);
178
179 mock_server_b->go();
180 connection.connect(30);
181 wait_for_server_open();
182 REQUIRE(connected_b);
183 mock_server_b.reset();
184 let_connection_stop(connection);
185
186 mock_server_a.reset(new MockServer(port_a));
__anoncab39b980502(websocketpp::connection_hdl hdl) 187 mock_server_a->set_open_handler([&connected_c](websocketpp::connection_hdl hdl) {
188 connected_c = true;
189 });
190 mock_server_a->go();
191 connection.connect(30);
192
193 wait_for_server_open();
194 REQUIRE(connected_c);
195 }
196 }
197
198 TEST_CASE("Connection::~Connection", "[connection]") {
199 SECTION("connect fails with connection timeout < server's processing time") {
200 MockServer mock_server;
201 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
202 getKeyPath(), WS_TIMEOUT_MS,
203 PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
204
205 // The WebSocket connection must not be established before 10 ms
206 mock_server.set_validate_handler(
__anoncab39b980602(websocketpp::connection_hdl hdl) 207 [](websocketpp::connection_hdl hdl) {
208 Util::this_thread::sleep_for(Util::chrono::milliseconds(10));
209 return true;
210 });
211
212 mock_server.go();
213 c_m.ws_connection_timeout_ms = 1;
214 Connection connection {
215 "wss://localhost:" + std::to_string(mock_server.port()) + "/pcp",
216 c_m };
217
218 // The connection timeout should fire and an onFail event
219 // should be triggered, followed by a connection_fatal_error
220 REQUIRE_THROWS_AS(connection.connect(1),
221 connection_fatal_error);
222
223 // Triggering the dtor
224 }
225
226 SECTION("succeeds with connection timeout = 990 ms") {
227 MockServer mock_server;
228 ClientMetadata c_m { "test_client", getCaPath(), getCertPath(),
229 getKeyPath(), WS_TIMEOUT_MS,
230 PONG_TIMEOUTS_BEFORE_RETRY, PONG_LONG_TIMEOUT_MS };
231 mock_server.go();
232 c_m.ws_connection_timeout_ms = 990;
233 Connection connection {
234 "wss://localhost:" + std::to_string(mock_server.port()) + "/pcp",
235 c_m };
236
237 REQUIRE_NOTHROW(connection.connect(1));
238
239 lth_util::Timer timer {};
240 while (connection.getConnectionState() == ConnectionState::connecting
241 && timer.elapsed_seconds() < 2)
242 Util::this_thread::sleep_for(Util::chrono::milliseconds(1));
243
244 auto c_s = connection.getConnectionState();
245 auto open_or_closed = c_s == ConnectionState::open
246 || c_s == ConnectionState::closed;
247 REQUIRE(open_or_closed);
248
249 // Triggering the dtor
250 }
251
252 // Connector's dtor should be able to effectively stop the
253 // event handler and join its thread
254 REQUIRE(true);
255 }
256