#include "doctest.h" #include #include #include namespace { DevelopmentHelloError parse(const std::string& json, DevelopmentHello& hello) { const char* message = nullptr; const DevelopmentHelloError error = development_protocol_parse_hello(json.c_str(), json.size(), &hello, &message); CHECK_EQ(error == DevelopmentHelloError::None, message == nullptr); return error; } } // namespace TEST_CASE("development_protocol_parse_hello accepts a shell hello") { DevelopmentHello hello {}; REQUIRE_EQ(parse(R"({"protocol":1,"mode":"shell","client":"ken@laptop","columns":120,"rows":40})", hello), DevelopmentHelloError::None); CHECK_EQ(hello.protocol, 1); CHECK_EQ(std::string(hello.mode), "shell"); CHECK_EQ(std::string(hello.client), "ken@laptop"); CHECK_EQ(hello.columns, 120); CHECK_EQ(hello.rows, 40); } TEST_CASE("development_protocol_parse_hello accepts sync and app hellos") { DevelopmentHello hello {}; REQUIRE_EQ(parse(R"({"protocol":1,"mode":"sync","client":"c"})", hello), DevelopmentHelloError::None); CHECK_EQ(std::string(hello.mode), "sync"); REQUIRE_EQ(parse(R"({"protocol":1,"mode":"app","client":"c"})", hello), DevelopmentHelloError::None); CHECK_EQ(std::string(hello.mode), "app"); } TEST_CASE("development_protocol_parse_hello accepts a command in shell mode only") { DevelopmentHello hello {}; REQUIRE_EQ(parse(R"({"protocol":1,"mode":"shell","client":"c","command":"echo hi"})", hello), DevelopmentHelloError::None); CHECK_EQ(std::string(hello.command), "echo hi"); REQUIRE_EQ(parse(R"({"protocol":1,"mode":"shell","client":"c"})", hello), DevelopmentHelloError::None); CHECK_EQ(std::string(hello.command), ""); CHECK_EQ(parse(R"({"protocol":1,"mode":"sync","client":"c","command":"echo hi"})", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"protocol":1,"mode":"shell","client":"c","command":3})", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"protocol":1,"mode":"shell","client":"c","command":")" + std::string(DEVELOPMENT_COMMAND_LENGTH + 1, 'x') + "\"}", hello), DevelopmentHelloError::Malformed); } TEST_CASE("development_protocol_parse_hello defaults a missing or invalid window size") { DevelopmentHello hello {}; REQUIRE_EQ(parse(R"({"protocol":1,"mode":"shell","client":"c","columns":0})", hello), DevelopmentHelloError::None); CHECK_EQ(hello.columns, 80); CHECK_EQ(hello.rows, 24); } TEST_CASE("development_protocol_parse_hello rejects invalid hellos") { DevelopmentHello hello {}; CHECK_EQ(parse("not json", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse("[]", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"mode":"shell","client":"c"})", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"protocol":1,"client":"c"})", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"protocol":1,"mode":"shell"})", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"protocol":1,"mode":"shell","client":")" + std::string(DEVELOPMENT_CLIENT_NAME_LENGTH + 1, 'x') + "\"}", hello), DevelopmentHelloError::Malformed); CHECK_EQ(parse(R"({"protocol":2,"mode":"shell","client":"c"})", hello), DevelopmentHelloError::UnsupportedProtocol); CHECK_EQ(parse(R"({"protocol":1,"mode":"ftp","client":"c"})", hello), DevelopmentHelloError::UnsupportedMode); } TEST_CASE("development_protocol_format_status writes a JSON line") { char line[64]; const size_t length = development_protocol_format_status("error", "busy", line, sizeof(line)); CHECK_EQ(std::string(line, length), "{\"status\":\"error\",\"message\":\"busy\"}\n"); CHECK_EQ(development_protocol_format_status("accepted", nullptr, line, sizeof(line)), strlen("{\"status\":\"accepted\"}\n")); CHECK_EQ(development_protocol_format_status("accepted", nullptr, line, 8), 0); } TEST_CASE("development_frame_parse handles complete, partial and oversized frames") { uint8_t data[DEVELOPMENT_FRAME_HEADER_SIZE + 3 + DEVELOPMENT_FRAME_HEADER_SIZE]; development_frame_write_header(DEVELOPMENT_FRAME_DATA, 3, data); memcpy(data + DEVELOPMENT_FRAME_HEADER_SIZE, "abc", 3); development_frame_write_header(DEVELOPMENT_FRAME_EXIT, 0, data + DEVELOPMENT_FRAME_HEADER_SIZE + 3); DevelopmentFrame frame {}; size_t consumed = 0; CHECK_EQ(development_frame_parse(data, 2, &frame, &consumed), DevelopmentFrameParseResult::Incomplete); CHECK_EQ(development_frame_parse(data, DEVELOPMENT_FRAME_HEADER_SIZE + 2, &frame, &consumed), DevelopmentFrameParseResult::Incomplete); REQUIRE_EQ(development_frame_parse(data, sizeof(data), &frame, &consumed), DevelopmentFrameParseResult::Ok); CHECK_EQ(frame.type, DEVELOPMENT_FRAME_DATA); CHECK_EQ(std::string(reinterpret_cast(frame.payload), frame.length), "abc"); CHECK_EQ(consumed, DEVELOPMENT_FRAME_HEADER_SIZE + 3); REQUIRE_EQ(development_frame_parse(data + consumed, sizeof(data) - consumed, &frame, &consumed), DevelopmentFrameParseResult::Ok); CHECK_EQ(frame.type, DEVELOPMENT_FRAME_EXIT); CHECK_EQ(frame.length, 0); CHECK_EQ(consumed, DEVELOPMENT_FRAME_HEADER_SIZE); uint8_t oversized[DEVELOPMENT_FRAME_HEADER_SIZE]; development_frame_write_header(DEVELOPMENT_FRAME_DATA, DEVELOPMENT_FRAME_PAYLOAD_MAX + 1, oversized); CHECK_EQ(development_frame_parse(oversized, sizeof(oversized), &frame, &consumed), DevelopmentFrameParseResult::Invalid); }