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