mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
445 lines
16 KiB
C++
445 lines
16 KiB
C++
/**
|
|
* GTest-based unit tests for EntitySchemaLoader.
|
|
* Covers: loadSchemas, loadSchema, parseJson, parseField, parseIndex, parseACL
|
|
*/
|
|
|
|
#include <gtest/gtest.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
|
|
#include "dbal/core/entity_loader.hpp"
|
|
|
|
using dbal::core::EntitySchemaLoader;
|
|
using dbal::core::EntitySchema;
|
|
using dbal::core::EntityField;
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static fs::path writeTmp(const std::string& filename, const std::string& content) {
|
|
auto p = fs::temp_directory_path() / filename;
|
|
std::ofstream(p) << content;
|
|
return p;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// loadSchemas
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_NonExistentDir_ReturnsEmpty) {
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas("/nonexistent/path/schemas");
|
|
EXPECT_TRUE(result.empty());
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_EmptyDir_ReturnsEmpty) {
|
|
auto dir = fs::temp_directory_path() / "el_empty_dir";
|
|
fs::create_directories(dir);
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_TRUE(result.empty());
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_ValidEntityJson_LoadsOne) {
|
|
auto dir = fs::temp_directory_path() / "el_valid_dir";
|
|
fs::create_directories(dir);
|
|
std::ofstream(dir / "widget.json") << R"({
|
|
"entity": "Widget",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true, "generated": true},
|
|
"label": {"type": "string", "required": true}
|
|
}
|
|
})";
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_EQ(result.size(), 1u);
|
|
EXPECT_TRUE(result.count("Widget"));
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_ArrayFormatJson_LoadsMultiple) {
|
|
auto dir = fs::temp_directory_path() / "el_array_dir";
|
|
fs::create_directories(dir);
|
|
std::ofstream(dir / "multi.json") << R"([
|
|
{"entity": "Alpha", "version": "1.0", "fields": {"id": {"type": "uuid", "primary": true}}},
|
|
{"entity": "Beta", "version": "1.0", "fields": {"id": {"type": "uuid", "primary": true}}}
|
|
])";
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_EQ(result.size(), 2u);
|
|
EXPECT_TRUE(result.count("Alpha"));
|
|
EXPECT_TRUE(result.count("Beta"));
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_InvalidJson_SkipsFile) {
|
|
auto dir = fs::temp_directory_path() / "el_bad_dir";
|
|
fs::create_directories(dir);
|
|
// Invalid JSON — should be caught and skipped, not crash
|
|
std::ofstream(dir / "bad.json") << "{ this is not json !!!";
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_TRUE(result.empty());
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_NoNameKey_SkipsEntity) {
|
|
auto dir = fs::temp_directory_path() / "el_noname_dir";
|
|
fs::create_directories(dir);
|
|
std::ofstream(dir / "noname.json") << R"({"version": "1.0", "fields": {"id": {"type": "uuid", "primary": true}}})";
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_TRUE(result.empty());
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// loadSchema
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_NonExistentFile_Throws) {
|
|
EntitySchemaLoader loader;
|
|
EXPECT_THROW(loader.loadSchema("/nonexistent/schema.json"), std::runtime_error);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_ValidFile_ReturnsSchema) {
|
|
auto path = writeTmp("el_valid.json", R"({
|
|
"entity": "Product",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true, "generated": true},
|
|
"name": {"type": "string", "required": true}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
EXPECT_EQ(schema.name, "Product");
|
|
EXPECT_EQ(schema.fields.size(), 2u);
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_ArrayFile_UsesFirstElement) {
|
|
auto path = writeTmp("el_arr.json", R"([
|
|
{"entity": "First", "version": "1.0", "fields": {"id": {"type": "uuid", "primary": true}}},
|
|
{"entity": "Second", "version": "1.0", "fields": {"id": {"type": "uuid", "primary": true}}}
|
|
])");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
EXPECT_EQ(schema.name, "First");
|
|
fs::remove(path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// parseJson — field variations
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_FieldWithDescription) {
|
|
auto path = writeTmp("el_desc.json", R"({
|
|
"entity": "Desc",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"note": {"type": "text", "description": "A helpful note"}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
bool found = false;
|
|
for (const auto& f : schema.fields)
|
|
if (f.name == "note") { EXPECT_TRUE(f.description.has_value()); found = true; }
|
|
EXPECT_TRUE(found);
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_FieldWithMinMaxLength) {
|
|
auto path = writeTmp("el_len.json", R"({
|
|
"entity": "Len",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"tag": {"type": "string", "min_length": 2, "max_length": 20}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
bool found = false;
|
|
for (const auto& f : schema.fields)
|
|
if (f.name == "tag") {
|
|
EXPECT_EQ(f.minLength.value_or(0), 2);
|
|
EXPECT_EQ(f.maxLength.value_or(0), 20);
|
|
found = true;
|
|
}
|
|
EXPECT_TRUE(found);
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_EnumField) {
|
|
auto path = writeTmp("el_enum.json", R"({
|
|
"entity": "Enum",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"status": {"type": "enum", "values": ["a", "b", "c"]}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
bool found = false;
|
|
for (const auto& f : schema.fields)
|
|
if (f.name == "status") {
|
|
ASSERT_TRUE(f.enumValues.has_value());
|
|
EXPECT_EQ(f.enumValues->size(), 3u);
|
|
found = true;
|
|
}
|
|
EXPECT_TRUE(found);
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_MetadataField) {
|
|
auto path = writeTmp("el_meta.json", R"({
|
|
"entity": "Meta",
|
|
"version": "1.0",
|
|
"fields": {"id": {"type": "uuid", "primary": true}},
|
|
"metadata": {"owner": "team-a", "tier": "core"}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
EXPECT_EQ(schema.metadata.at("owner"), "team-a");
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_WithIndexes) {
|
|
auto path = writeTmp("el_idx.json", R"({
|
|
"entity": "Idx",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"email": {"type": "email"}
|
|
},
|
|
"indexes": [{"fields": ["email"], "unique": true, "name": "idx_email"}]
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
ASSERT_EQ(schema.indexes.size(), 1u);
|
|
EXPECT_TRUE(schema.indexes[0].unique);
|
|
EXPECT_EQ(schema.indexes[0].name.value_or(""), "idx_email");
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_WithACL) {
|
|
auto path = writeTmp("el_acl.json", R"({
|
|
"entity": "Acl",
|
|
"version": "1.0",
|
|
"fields": {"id": {"type": "uuid", "primary": true}},
|
|
"acl": {
|
|
"create": {"admin": true, "user": false},
|
|
"read": {"admin": true, "user": true},
|
|
"update": {"admin": true, "user": false},
|
|
"delete": {"admin": true}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
ASSERT_TRUE(schema.acl.has_value());
|
|
EXPECT_TRUE(schema.acl->create.at("admin"));
|
|
EXPECT_FALSE(schema.acl->create.at("user"));
|
|
EXPECT_TRUE(schema.acl->read.at("user"));
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_NameFromNameKey) {
|
|
auto path = writeTmp("el_namekey.json", R"({
|
|
"name": "ByName",
|
|
"version": "1.0",
|
|
"fields": {"id": {"type": "uuid", "primary": true}}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
EXPECT_EQ(schema.name, "ByName");
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_DisplayNameAndDescription) {
|
|
auto path = writeTmp("el_dispname.json", R"({
|
|
"entity": "Disp",
|
|
"displayName": "Display Widget",
|
|
"description": "A widget for display",
|
|
"version": "1.0",
|
|
"fields": {"id": {"type": "uuid", "primary": true}}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
EXPECT_EQ(schema.displayName, "Display Widget");
|
|
EXPECT_EQ(schema.description, "A widget for display");
|
|
fs::remove(path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// getDefaultSchemaPath — smoke test (doesn't throw if schema dir exists)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, GetDefaultSchemaPath_ThrowsIfNotFound) {
|
|
// The test binary runs from _build/ which likely doesn't have the schema dir
|
|
// nearby at expected relative paths — it should either succeed (if dir exists)
|
|
// or throw std::runtime_error. We just verify no UB.
|
|
try {
|
|
auto path = EntitySchemaLoader::getDefaultSchemaPath();
|
|
EXPECT_FALSE(path.empty());
|
|
} catch (const std::runtime_error&) {
|
|
// Expected when running from _build/ without the schema dir accessible
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Field parser edge cases via loadSchema
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, FieldParser_NonStringDefault_Dumped) {
|
|
auto path = writeTmp("el_numdef.json", R"({
|
|
"entity": "NumDef",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"count": {"type": "integer", "default": 42}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
for (const auto& f : schema.fields)
|
|
if (f.name == "count") EXPECT_TRUE(f.defaultValue.has_value());
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, FieldParser_BoolFlags_AllSet) {
|
|
auto path = writeTmp("el_flags.json", R"({
|
|
"entity": "Flags",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true, "generated": true, "unique": true, "nullable": true, "index": true}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
auto& f = schema.fields[0];
|
|
EXPECT_TRUE(f.primary);
|
|
EXPECT_TRUE(f.generated);
|
|
EXPECT_TRUE(f.unique);
|
|
EXPECT_TRUE(f.nullable);
|
|
EXPECT_TRUE(f.index);
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, FieldParser_MinLengthAltKey) {
|
|
// Covers the "minLength" (camelCase) branch vs "min_length" (snake_case)
|
|
auto path = writeTmp("el_minlen.json", R"({
|
|
"entity": "MinLen",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"slug": {"type": "string", "minLength": 3, "maxLength": 50, "pattern": "^[a-z]+$"}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
for (const auto& f : schema.fields)
|
|
if (f.name == "slug") {
|
|
EXPECT_EQ(f.minLength.value_or(0), 3);
|
|
EXPECT_EQ(f.maxLength.value_or(0), 50);
|
|
EXPECT_TRUE(f.pattern.has_value());
|
|
}
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, FieldParser_References) {
|
|
auto path = writeTmp("el_ref.json", R"({
|
|
"entity": "Ref",
|
|
"version": "1.0",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true},
|
|
"userId": {"type": "uuid", "references": "User.id"}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
auto schema = loader.loadSchema(path.string());
|
|
for (const auto& f : schema.fields)
|
|
if (f.name == "userId") {
|
|
ASSERT_TRUE(f.references.has_value());
|
|
EXPECT_EQ(f.references.value(), "User.id");
|
|
}
|
|
fs::remove(path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// loadSchemas — exercises findJsonFiles and parseJson indirectly (lines 99-101)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_MultipleFilesInDir_CountsAll) {
|
|
auto dir = fs::temp_directory_path() / "el_multi_count";
|
|
fs::create_directories(dir);
|
|
std::ofstream(dir / "alpha.json") << R"({"entity":"Alpha","version":"1.0","fields":{"id":{"type":"uuid","primary":true}}})";
|
|
std::ofstream(dir / "beta.json") << R"({"entity":"Beta", "version":"1.0","fields":{"id":{"type":"uuid","primary":true}}})";
|
|
std::ofstream(dir / "entities.json") << "{}"; // excluded by findJsonFiles
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_EQ(result.size(), 2u);
|
|
EXPECT_TRUE(result.count("Alpha"));
|
|
EXPECT_TRUE(result.count("Beta"));
|
|
fs::remove_all(dir);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// loadSchema — validation failure throws (lines 88-92)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_ValidationFailure_Throws) {
|
|
// Schema with no fields → fails validation ("has no fields defined")
|
|
auto path = writeTmp("el_nofields.json", R"({
|
|
"entity": "Empty",
|
|
"version": "1.0",
|
|
"fields": {}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
EXPECT_THROW(loader.loadSchema(path.string()), std::runtime_error);
|
|
fs::remove(path);
|
|
}
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchema_ValidationWarning_NoThrow) {
|
|
// Schema with no version → validation warning (not error), should load fine
|
|
auto path = writeTmp("el_noversion.json", R"({
|
|
"entity": "NoVer",
|
|
"version": "",
|
|
"fields": {
|
|
"id": {"type": "uuid", "primary": true}
|
|
}
|
|
})");
|
|
EntitySchemaLoader loader;
|
|
// Warning-only: should not throw
|
|
EXPECT_NO_THROW(loader.loadSchema(path.string()));
|
|
fs::remove(path);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// loadSchemas — validation error branch (lines 56-57) and warning branch (59)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
TEST(EntitySchemaLoaderTest, LoadSchemas_ValidationError_SkipsEntity) {
|
|
auto dir = fs::temp_directory_path() / "el_valerr_dir";
|
|
fs::create_directories(dir);
|
|
// This schema has no fields → fails SchemaValidator → should be skipped
|
|
std::ofstream(dir / "bad_schema.json") << R"({
|
|
"entity": "NoFields",
|
|
"version": "1.0",
|
|
"fields": {}
|
|
})";
|
|
EntitySchemaLoader loader;
|
|
auto result = loader.loadSchemas(dir.string());
|
|
EXPECT_TRUE(result.empty());
|
|
fs::remove_all(dir);
|
|
}
|