164 lines
5.9 KiB
C++
164 lines
5.9 KiB
C++
/*
|
|||
|
|
streamer-tools OBS Camera Plugin - JSON reader tests
|
||
|
|
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
|
||
|
|
|
||
|
|
This program is free software; you can redistribute it and/or modify
|
||
|
|
it under the terms of the GNU General Public License as published by
|
||
|
|
the Free Software Foundation; either version 2 of the License, or
|
||
|
|
(at your option) any later version.
|
||
|
|
|
||
|
|
This program is distributed in the hope that it will be useful,
|
||
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
GNU General Public License for more details.
|
||
|
|
|
||
|
|
You should have received a copy of the GNU General Public License along
|
||
|
|
with this program. If not, see <https://www.gnu.org/licenses/>
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <string>
|
||
|
|
|
||
|
|
#include "stplugin/json.h"
|
||
|
|
#include "test_util.h"
|
||
|
|
|
||
|
|
using stplugin::json::Value;
|
||
|
|
using stplugin::json::parse;
|
||
|
|
|
||
|
|
static void testRealResponses()
|
||
|
|
{
|
||
|
|
// The exact shape apps/server/src/obs/plugin.routes.ts returns.
|
||
|
|
const Value slots = parse(
|
||
|
|
R"({"slots":[{"identity":"cam1","displayName":"Alice","live":true},)"
|
||
|
|
R"({"identity":"cam2","displayName":"Bob","live":false}]})");
|
||
|
|
ST_ASSERT(slots.valid());
|
||
|
|
ST_ASSERT(slots.isObject());
|
||
|
|
ST_ASSERT(slots["slots"].isArray());
|
||
|
|
ST_ASSERT_EQ(slots["slots"].size(), std::size_t(2));
|
||
|
|
ST_ASSERT_EQ(slots["slots"].at(0)["identity"].asString(), std::string("cam1"));
|
||
|
|
ST_ASSERT_EQ(slots["slots"].at(0)["displayName"].asString(), std::string("Alice"));
|
||
|
|
ST_ASSERT_EQ(slots["slots"].at(0)["live"].asBool(), true);
|
||
|
|
ST_ASSERT_EQ(slots["slots"].at(1)["live"].asBool(true), false);
|
||
|
|
|
||
|
|
const Value token = parse(
|
||
|
|
R"({"lkToken":"eyJhbGciOiJIUzI1NiJ9.abc.def","wsUrl":"wss://streamers.example.com",)"
|
||
|
|
R"("identity":"obs:main-room:Ab_1-cd2"})");
|
||
|
|
ST_ASSERT_EQ(token["lkToken"].asString(), std::string("eyJhbGciOiJIUzI1NiJ9.abc.def"));
|
||
|
|
ST_ASSERT_EQ(token["wsUrl"].asString(), std::string("wss://streamers.example.com"));
|
||
|
|
ST_ASSERT_EQ(token["identity"].asString(), std::string("obs:main-room:Ab_1-cd2"));
|
||
|
|
|
||
|
|
const Value error = parse(R"({"error":"not found"})");
|
||
|
|
ST_ASSERT_EQ(error["error"].asString(), std::string("not found"));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void testScalarsAndEscapes()
|
||
|
|
{
|
||
|
|
ST_ASSERT(parse("null").isNull());
|
||
|
|
ST_ASSERT_EQ(parse("true").asBool(), true);
|
||
|
|
ST_ASSERT_EQ(parse("false").asBool(true), false);
|
||
|
|
ST_ASSERT_EQ(parse("0").asNumber(), 0.0);
|
||
|
|
ST_ASSERT_EQ(parse("-12").asNumber(), -12.0);
|
||
|
|
ST_ASSERT_EQ(parse("1.5e2").asNumber(), 150.0);
|
||
|
|
ST_ASSERT_EQ(parse("\"\"").asString("x"), std::string(""));
|
||
|
|
ST_ASSERT_EQ(parse(R"("a\"b\\c\/d")").asString(), std::string("a\"b\\c/d"));
|
||
|
|
ST_ASSERT_EQ(parse(R"("\n\t\r\b\f")").asString(), std::string("\n\t\r\b\f"));
|
||
|
|
|
||
|
|
// \u escapes, including a surrogate pair (an emoji in a display name is
|
||
|
|
// entirely plausible and must not corrupt the dropdown).
|
||
|
|
ST_ASSERT_EQ(parse(R"("\u0041")").asString(), std::string("A"));
|
||
|
|
ST_ASSERT_EQ(parse(R"("caf\u00e9")").asString(), std::string("caf\xc3\xa9"));
|
||
|
|
ST_ASSERT_EQ(parse(R"("\ud83d\ude00")").asString(), std::string("\xf0\x9f\x98\x80"));
|
||
|
|
|
||
|
|
// Whitespace everywhere legal.
|
||
|
|
ST_ASSERT_EQ(parse(" {\n \"a\" :\t[ 1 , 2 ]\r\n} ")["a"].size(), std::size_t(2));
|
||
|
|
}
|
||
|
|
|
||
|
|
static void testMalformedIsRejectedNotCrashed()
|
||
|
|
{
|
||
|
|
const char *bad[] = {
|
||
|
|
"",
|
||
|
|
" ",
|
||
|
|
"{",
|
||
|
|
"}",
|
||
|
|
"[",
|
||
|
|
"[1,",
|
||
|
|
"[1,]",
|
||
|
|
"{\"a\"}",
|
||
|
|
"{\"a\":}",
|
||
|
|
"{\"a\":1,}",
|
||
|
|
"{a:1}",
|
||
|
|
"{'a':1}",
|
||
|
|
"\"unterminated",
|
||
|
|
"\"bad\\escape\"",
|
||
|
|
"\"\\u00\"",
|
||
|
|
"\"\\uZZZZ\"",
|
||
|
|
"\"\\ud83d\"", // lone high surrogate
|
||
|
|
"\"\\ude00\"", // lone low surrogate
|
||
|
|
"01", // leading zero
|
||
|
|
"+1",
|
||
|
|
".5",
|
||
|
|
"1.",
|
||
|
|
"1e",
|
||
|
|
"1e+",
|
||
|
|
"tru",
|
||
|
|
"nulll",
|
||
|
|
"{}garbage", // trailing content
|
||
|
|
"[1,2] [3]",
|
||
|
|
"\"raw\ncontrol\"", // literal control char inside a string
|
||
|
|
"\xff\xfe", // binary garbage, e.g. an HTML error page prefix
|
||
|
|
"<!DOCTYPE html><html><body>502 Bad Gateway</body></html>",
|
||
|
|
};
|
||
|
|
for (const char *text : bad) {
|
||
|
|
const Value v = parse(text);
|
||
|
|
ST_ASSERT(!v.valid());
|
||
|
|
// Accessors on an invalid value must still be safe and return the
|
||
|
|
// caller's fallback.
|
||
|
|
ST_ASSERT_EQ(v["anything"].asString("fallback"), std::string("fallback"));
|
||
|
|
ST_ASSERT_EQ(v.at(0).asNumber(-1.0), -1.0);
|
||
|
|
ST_ASSERT_EQ(v.size(), std::size_t(0));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void testDepthLimit()
|
||
|
|
{
|
||
|
|
// Deep-but-legal nesting is rejected rather than recursed into, so a
|
||
|
|
// hostile response cannot overflow the stack inside OBS.
|
||
|
|
std::string deep;
|
||
|
|
const int depth = stplugin::json::kMaxDepth + 50;
|
||
|
|
for (int i = 0; i < depth; ++i)
|
||
|
|
deep += "[";
|
||
|
|
for (int i = 0; i < depth; ++i)
|
||
|
|
deep += "]";
|
||
|
|
ST_ASSERT(!parse(deep).valid());
|
||
|
|
|
||
|
|
// Just inside the limit still parses.
|
||
|
|
std::string shallow;
|
||
|
|
for (int i = 0; i < stplugin::json::kMaxDepth - 1; ++i)
|
||
|
|
shallow += "[";
|
||
|
|
shallow += "1";
|
||
|
|
for (int i = 0; i < stplugin::json::kMaxDepth - 1; ++i)
|
||
|
|
shallow += "]";
|
||
|
|
ST_ASSERT(parse(shallow).valid());
|
||
|
|
}
|
||
|
|
|
||
|
|
static void testWrongTypesFallBack()
|
||
|
|
{
|
||
|
|
const Value v = parse(R"({"n":5,"s":"x","b":true,"arr":[1],"obj":{}})");
|
||
|
|
ST_ASSERT_EQ(v["n"].asString("fallback"), std::string("fallback"));
|
||
|
|
ST_ASSERT_EQ(v["s"].asNumber(-1.0), -1.0);
|
||
|
|
ST_ASSERT_EQ(v["s"].asBool(true), true);
|
||
|
|
ST_ASSERT_EQ(v["missing"].asString("fallback"), std::string("fallback"));
|
||
|
|
ST_ASSERT_EQ(v["arr"].at(5).asNumber(-1.0), -1.0);
|
||
|
|
ST_ASSERT_EQ(v["obj"].at(0).asNumber(-1.0), -1.0);
|
||
|
|
ST_ASSERT_EQ(v["n"]["deeper"].asString("fallback"), std::string("fallback"));
|
||
|
|
}
|
||
|
|
|
||
|
|
int main()
|
||
|
|
{
|
||
|
|
testRealResponses();
|
||
|
|
testScalarsAndEscapes();
|
||
|
|
testMalformedIsRejectedNotCrashed();
|
||
|
|
testDepthLimit();
|
||
|
|
testWrongTypesFallBack();
|
||
|
|
return st_test_report("json");
|
||
|
|
}
|