Files
obs-streamer-tools-plugin/core/tests/test_json.cpp
T

182 lines
6.9 KiB
C++
Raw Normal View History

/*
streamer-tools OBS Camera Plugin - JSON reader tests
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
*/
#include <string>
#include <vector>
#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"));
}
// One backslash, as it appears in the JSON *text* being parsed.
//
// Every JSON input below that contains a backslash is built by concatenation
// rather than written as a literal. Two separate portability problems make
// the obvious spellings unsafe, both observed on the Windows CI runner:
// - MSVC still forms escape sequences and universal-character-names inside
// RAW string literals, which it must not: R"( ... backslash-u-d-8-3-d ... )"
// is a hard compile error ("a universal-character-name specifies an
// invalid character"), and a raw string containing backslash-slash is an
// "illegal escape sequence".
// - A doubled backslash immediately followed by 'u' inside an ordinary
// literal sits on a genuinely murky corner of translation phase 1, where
// compilers have historically disagreed about whether a
// universal-character-name is formed.
// Concatenation sidesteps both: no backslash is ever adjacent to a 'u' in
// the source text at all.
static const std::string kBS = "\\";
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(""));
// "a\"b\\c\/d" -> a"b\c/d
ST_ASSERT_EQ(parse("\"a" + kBS + "\"b" + kBS + kBS + "c" + kBS + "/d\"").asString(),
std::string("a\"b\\c/d"));
// "\n\t\r\b\f"
ST_ASSERT_EQ(parse("\"" + kBS + "n" + kBS + "t" + kBS + "r" + kBS + "b" + kBS + "f\"").asString(),
std::string("\n\t\r\b\f"));
// \uXXXX escapes, including a surrogate pair (an emoji in a display name
// is entirely plausible and must not corrupt the dropdown).
ST_ASSERT_EQ(parse("\"" + kBS + "u0041\"").asString(), std::string("A"));
ST_ASSERT_EQ(parse("\"caf" + kBS + "u00e9\"").asString(), std::string("caf\xc3\xa9"));
ST_ASSERT_EQ(parse("\"" + kBS + "ud83d" + kBS + "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 std::vector<std::string> bad = {
"",
" ",
"{",
"}",
"[",
"[1,",
"[1,]",
"{\"a\"}",
"{\"a\":}",
"{\"a\":1,}",
"{a:1}",
"{'a':1}",
"\"unterminated",
"\"bad" + kBS + "escape\"", // not a JSON escape character
"\"" + kBS + "u00\"", // truncated code point
"\"" + kBS + "uZZZZ\"", // non-hex code point
"\"" + kBS + "ud83d\"", // lone high surrogate
"\"" + kBS + "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
"<!DOCTYPE html><html><body>502 Bad Gateway</body></html>",
};
for (const std::string &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");
}