Initial vibecoded proof of concept

This commit is contained in:
Alex Selimov 2025-10-05 20:16:33 -04:00
parent 74812459af
commit 461318a656
Signed by: aselimov
GPG key ID: 3DDB9C3E023F1F31
61 changed files with 13306 additions and 0 deletions

View file

@ -0,0 +1,324 @@
-- Unit tests for caching utilities
local cache = require('notex.utils.cache')
describe("cache utilities", function()
before_each(function()
-- Reset cache before each test
cache.init({
memory = {enabled = true, max_size = 10},
lru = {enabled = true, max_size = 5},
timed = {enabled = true, default_ttl = 1}
})
end)
after_each(function()
cache.cleanup()
end)
describe("memory cache", function()
it("should store and retrieve values", function()
local ok = cache.memory_set("test_key", "test_value")
assert.is_true(ok)
local value = cache.memory_get("test_key")
assert.equals("test_value", value)
end)
it("should return nil for non-existent keys", function()
local value = cache.memory_get("non_existent")
assert.is_nil(value)
end)
it("should handle cache size limits", function()
-- Fill cache beyond max size
for i = 1, 15 do
cache.memory_set("key_" .. i, "value_" .. i)
end
-- Should still be able to set new values (eviction happens)
local ok = cache.memory_set("new_key", "new_value")
assert.is_true(ok)
-- Should still be able to get some values
local value = cache.memory_get("new_key")
assert.equals("new_value", value)
end)
end)
describe("LRU cache", function()
it("should store and retrieve values with LRU eviction", function()
cache.lru_set("key1", "value1")
cache.lru_set("key2", "value2")
cache.lru_set("key3", "value3")
-- Access key1 to make it most recently used
local value = cache.lru_get("key1")
assert.equals("value1", value)
-- Add more items to trigger eviction
cache.lru_set("key4", "value4")
cache.lru_set("key5", "value5")
cache.lru_set("key6", "value6") -- Should evict key2 (least recently used)
-- key2 should be evicted, key1 should still exist
assert.is_nil(cache.lru_get("key2"))
assert.equals("value1", cache.lru_get("key1"))
end)
it("should update access order on get", function()
cache.lru_set("key1", "value1")
cache.lru_set("key2", "value2")
-- Get key1 to make it most recently used
cache.lru_get("key1")
-- Fill cache to evict
cache.lru_set("key3", "value3")
cache.lru_set("key4", "value4")
cache.lru_set("key5", "value4")
cache.lru_set("key6", "value4") -- Should evict key2
assert.is_not_nil(cache.lru_get("key1")) -- Should still exist
assert.is_nil(cache.lru_get("key2")) -- Should be evicted
end)
end)
describe("timed cache", function()
it("should store values with TTL", function()
cache.timed_set("test_key", "test_value", 2) -- 2 second TTL
local value = cache.timed_get("test_key")
assert.equals("test_value", value)
end)
it("should expire values after TTL", function()
cache.timed_set("test_key", "test_value", 1) -- 1 second TTL
-- Should be available immediately
local value = cache.timed_get("test_key")
assert.equals("test_value", value)
-- Wait for expiration
vim.defer_fn(function()
value = cache.timed_get("test_key")
assert.is_nil(value)
end, 1100)
end)
it("should use default TTL when not specified", function()
cache.timed_set("test_key", "test_value")
local value = cache.timed_get("test_key")
assert.equals("test_value", value)
end)
end)
describe("generic cache operations", function()
it("should set and get with specified cache type", function()
local ok = cache.set("test_key", "test_value", "memory")
assert.is_true(ok)
local value = cache.get("test_key", "memory")
assert.equals("test_value", value)
end)
it("should default to memory cache", function()
local ok = cache.set("test_key", "test_value")
assert.is_true(ok)
local value = cache.get("test_key")
assert.equals("test_value", value)
end)
it("should handle unknown cache types", function()
local ok = cache.set("test_key", "test_value", "unknown")
assert.is_false(ok)
local value = cache.get("test_key", "unknown")
assert.is_nil(value)
end)
end)
describe("get_or_set", function()
it("should return cached value when exists", function()
cache.set("test_key", "cached_value")
local value = cache.get_or_set("test_key", function()
return "computed_value"
end)
assert.equals("cached_value", value)
end)
it("should compute and cache value when not exists", function()
local call_count = 0
local value = cache.get_or_set("test_key", function()
call_count = call_count + 1
return "computed_value"
end)
assert.equals("computed_value", value)
assert.equals(1, call_count)
-- Second call should use cached value
value = cache.get_or_set("test_key", function()
call_count = call_count + 1
return "new_value"
end)
assert.equals("computed_value", value)
assert.equals(1, call_count) -- Should not be called again
end)
it("should handle computation errors", function()
assert.has_error(function()
cache.get_or_set("test_key", function()
error("Computation failed")
end)
end)
end)
end)
describe("multi_get", function()
it("should try multiple cache types in order", function()
cache.set("test_key", "memory_value", "memory")
cache.set("test_key", "lru_value", "lru")
local value, cache_type = cache.multi_get("test_key", {"lru", "memory"})
assert.equals("lru_value", value)
assert.equals("lru", cache_type)
-- Should try memory if lru doesn't have it
cache.invalidate("test_key", "lru")
value, cache_type = cache.multi_get("test_key", {"lru", "memory"})
assert.equals("memory_value", value)
assert.equals("memory", cache_type)
end)
it("should return nil when not found in any cache", function()
local value = cache.multi_get("non_existent", {"memory", "lru", "timed"})
assert.is_nil(value)
end)
it("should use default cache types when not specified", function()
cache.set("test_key", "memory_value", "memory")
local value = cache.multi_get("test_key")
assert.equals("memory_value", value)
end)
end)
describe("invalidate", function()
it("should invalidate from specific cache type", function()
cache.set("test_key", "test_value", "memory")
cache.set("test_key", "test_value", "lru")
cache.invalidate("test_key", "memory")
assert.is_nil(cache.get("test_key", "memory"))
assert.equals("test_value", cache.get("test_key", "lru"))
end)
it("should invalidate from all caches when type not specified", function()
cache.set("test_key", "test_value", "memory")
cache.set("test_key", "test_value", "lru")
cache.set("test_key", "test_value", "timed")
cache.invalidate("test_key")
assert.is_nil(cache.get("test_key", "memory"))
assert.is_nil(cache.get("test_key", "lru"))
assert.is_nil(cache.get("test_key", "timed"))
end)
end)
describe("clear_all", function()
it("should clear all caches", function()
cache.set("key1", "value1", "memory")
cache.set("key2", "value2", "lru")
cache.set("key3", "value3", "timed")
cache.clear_all()
assert.is_nil(cache.get("key1", "memory"))
assert.is_nil(cache.get("key2", "lru"))
assert.is_nil(cache.get("key3", "timed"))
end)
it("should reset metrics", function()
-- Generate some metrics
cache.set("test", "value")
cache.get("test")
cache.get("non_existent")
local stats_before = cache.get_stats()
assert.is_true(stats_before.metrics.hits > 0)
assert.is_true(stats_before.metrics.misses > 0)
cache.clear_all()
local stats_after = cache.get_stats()
assert.equals(0, stats_after.metrics.hits)
assert.equals(0, stats_after.metrics.misses)
end)
end)
describe("get_stats", function()
it("should return comprehensive statistics", function()
-- Generate some activity
cache.set("test1", "value1")
cache.set("test2", "value2")
cache.get("test1")
cache.get("non_existent")
local stats = cache.get_stats()
assert.is_not_nil(stats.metrics)
assert.is_not_nil(stats.sizes)
assert.is_not_nil(stats.config)
assert.is_true(stats.metrics.sets >= 2)
assert.is_true(stats.metrics.hits >= 1)
assert.is_true(stats.metrics.misses >= 1)
assert.is_not_nil(stats.metrics.hit_ratio)
end)
it("should calculate hit ratio correctly", function()
-- Generate known metrics
cache.set("test", "value")
cache.get("test") -- hit
cache.get("test") -- hit
cache.get("non_existent") -- miss
local stats = cache.get_stats()
-- Should be 2 hits out of 3 total requests = ~0.67
assert.is_true(stats.metrics.hit_ratio > 0.6)
assert.is_true(stats.metrics.hit_ratio < 0.7)
end)
end)
describe("configuration", function()
it("should initialize with custom configuration", function()
cache.init({
memory = {enabled = false},
lru = {enabled = true, max_size = 20},
timed = {enabled = true, default_ttl = 10}
})
-- Memory cache should be disabled
local ok = cache.memory_set("test", "value")
assert.is_false(ok)
-- LRU cache should work with new size
ok = cache.lru_set("test", "value")
assert.is_true(ok)
-- Timed cache should work with new TTL
cache.timed_set("test2", "value")
local value = cache.timed_get("test2")
assert.equals("value", value)
end)
end)
end)

View file

@ -0,0 +1,277 @@
-- Unit tests for date parsing and formatting utilities
local date_utils = require('notex.utils.date')
describe("date utilities", function()
describe("parse_date", function()
it("should parse ISO 8601 dates", function()
local timestamp = date_utils.parse_date("2023-12-25")
assert.is_not_nil(timestamp)
local formatted = os.date("%Y-%m-%d", timestamp)
assert.equals("2023-12-25", formatted)
end)
it("should parse ISO 8601 datetimes", function()
local timestamp = date_utils.parse_date("2023-12-25T10:30:00")
assert.is_not_nil(timestamp)
local formatted = os.date("%Y-%m-%d %H:%M:%S", timestamp)
assert.equals("2023-12-25 10:30:00", formatted)
end)
it("should parse ISO 8601 with timezone", function()
local timestamp = date_utils.parse_date("2023-12-25T10:30:00+02:00")
assert.is_not_nil(timestamp)
-- Should be converted to UTC
local utc_formatted = os.date("%Y-%m-%d %H:%M:%S", timestamp)
assert.equals("2023-12-25 08:30:00", utc_formatted)
end)
it("should parse relative dates", function()
-- Test 1 day ago
local one_day_ago = os.time() - 86400
local timestamp = date_utils.parse_date("1d")
assert.is_true(math.abs(timestamp - one_day_ago) < 60) -- Within 1 minute
-- Test 1 hour ago
local one_hour_ago = os.time() - 3600
timestamp = date_utils.parse_date("1h")
assert.is_true(math.abs(timestamp - one_hour_ago) < 60) -- Within 1 minute
end)
it("should parse natural dates", function()
local timestamp = date_utils.parse_date("2023-12-25")
assert.is_not_nil(timestamp)
local formatted = os.date("%Y-%m-%d", timestamp)
assert.equals("2023-12-25", formatted)
end)
it("should return nil for invalid dates", function()
local timestamp = date_utils.parse_date("invalid date")
assert.is_nil(timestamp)
timestamp = date_utils.parse_date("")
assert.is_nil(timestamp)
timestamp = date_utils.parse_date(nil)
assert.is_nil(timestamp)
end)
it("should parse common date formats", function()
-- MM/DD/YYYY
local timestamp = date_utils.parse_date("12/25/2023")
assert.is_not_nil(timestamp)
-- MM-DD-YYYY
timestamp = date_utils.parse_date("12-25-2023")
assert.is_not_nil(timestamp)
end)
end)
describe("format_date", function()
it("should format timestamp to string", function()
local timestamp = os.time({year = 2023, month = 12, day = 25, hour = 10, min = 30, sec = 0})
local formatted = date_utils.format_date(timestamp, "%Y-%m-%d %H:%M:%S")
assert.equals("2023-12-25 10:30:00", formatted)
end)
it("should use default format", function()
local timestamp = os.time({year = 2023, month = 12, day = 25})
local formatted = date_utils.format_date(timestamp)
assert.equals("2023-12-25", formatted)
end)
it("should handle nil timestamp", function()
local formatted = date_utils.format_date(nil)
assert.equals("", formatted)
end)
end)
describe("get_relative_time", function()
it("should return 'just now' for recent times", function()
local current_time = os.time()
local relative = date_utils.get_relative_time(current_time)
assert.equals("just now", relative)
end)
it("should return minutes ago", function()
local timestamp = os.time() - 120 -- 2 minutes ago
local relative = date_utils.get_relative_time(timestamp)
assert.equals("2 minutes ago", relative)
end)
it("should return hours ago", function()
local timestamp = os.time() - 7200 -- 2 hours ago
local relative = date_utils.get_relative_time(timestamp)
assert.equals("2 hours ago", relative)
end)
it("should return days ago", function()
local timestamp = os.time() - 172800 -- 2 days ago
local relative = date_utils.get_relative_time(timestamp)
assert.equals("2 days ago", relative)
end)
it("should return months ago", function()
local timestamp = os.time() - (60 * 86400) -- ~2 months ago
local relative = date_utils.get_relative_time(timestamp)
assert.matches("months ago", relative)
end)
it("should return years ago", function()
local timestamp = os.time() - (365 * 86400) -- ~1 year ago
local relative = date_utils.get_relative_time(timestamp)
assert.matches("year ago", relative)
end)
it("should handle singular forms", function()
local timestamp = os.time() - 60 -- 1 minute ago
local relative = date_utils.get_relative_time(timestamp)
assert.equals("1 minute ago", relative)
timestamp = os.time() - 3600 -- 1 hour ago
relative = date_utils.get_relative_time(timestamp)
assert.equals("1 hour ago", relative)
end)
end)
describe("get_month_number", function()
it("should convert month names to numbers", function()
assert.equals(1, date_utils.get_month_number("January"))
assert.equals(12, date_utils.get_month_number("December"))
assert.equals(6, date_utils.get_month_number("June"))
end)
it("should handle short month names", function()
assert.equals(1, date_utils.get_month_number("Jan"))
assert.equals(12, date_utils.get_month_number("Dec"))
assert.equals(6, date_utils.get_month_number("Jun"))
end)
it("should be case insensitive", function()
assert.equals(1, date_utils.get_month_number("JANUARY"))
assert.equals(1, date_utils.get_month_number("january"))
assert.equals(1, date_utils.get_month_number("jan"))
end)
it("should return nil for invalid month names", function()
assert.is_nil(date_utils.get_month_number("NotAMonth"))
assert.is_nil(date_utils.get_month_number(""))
end)
end)
describe("get_month_name", function()
it("should convert month numbers to names", function()
assert.equals("January", date_utils.get_month_name(1))
assert.equals("December", date_utils.get_month_name(12))
assert.equals("June", date_utils.get_month_name(6))
end)
it("should return short names when requested", function()
assert.equals("Jan", date_utils.get_month_name(1, true))
assert.equals("Dec", date_utils.get_month_name(12, true))
assert.equals("Jun", date_utils.get_month_name(6, true))
end)
it("should return nil for invalid month numbers", function()
assert.is_nil(date_utils.get_month_name(0))
assert.is_nil(date_utils.get_month_name(13))
assert.is_nil(date_utils.get_month_name(-1))
end)
end)
describe("is_valid_date", function()
it("should validate correct dates", function()
assert.is_true(date_utils.is_valid_date("2023-12-25"))
assert.is_true(date_utils.is_valid_date("2023-12-25T10:30:00"))
assert.is_true(date_utils.is_valid_date("1d"))
end)
it("should reject invalid dates", function()
assert.is_false(date_utils.is_valid_date("invalid"))
assert.is_false(date_utils.is_valid_date(""))
assert.is_false(date_utils.is_valid_date(nil))
end)
end)
describe("add_time", function()
it("should add time to timestamp", function()
local timestamp = os.time({year = 2023, month = 12, day = 25})
local new_timestamp = date_utils.add_time(timestamp, 1, "days")
local expected = os.time({year = 2023, month = 12, day = 26})
assert.equals(expected, new_timestamp)
end)
it("should add different time units", function()
local timestamp = os.time({year = 2023, month = 12, day = 25, hour = 10})
-- Add hours
local new_timestamp = date_utils.add_time(timestamp, 2, "hours")
local expected = os.time({year = 2023, month = 12, day = 25, hour = 12})
assert.equals(expected, new_timestamp)
-- Add minutes
new_timestamp = date_utils.add_time(timestamp, 30, "minutes")
expected = os.time({year = 2023, month = 12, day = 25, hour = 10, min = 30})
assert.equals(expected, new_timestamp)
end)
end)
describe("get_date_range", function()
it("should calculate date range", function()
local range = date_utils.get_date_range("2023-12-25", "2023-12-27")
assert.is_not_nil(range)
assert.equals(2, range.duration_days)
assert.equals("2023-12-25", range.start_formatted)
assert.equals("2023-12-27", range.end_formatted)
end)
it("should return nil for invalid dates", function()
local range = date_utils.get_date_range("invalid", "2023-12-27")
assert.is_nil(range)
end)
end)
describe("get_week_bounds", function()
it("should get week start and end", function()
local week_bounds = date_utils.get_week_bounds()
assert.is_not_nil(week_bounds.start_timestamp)
assert.is_not_nil(week_bounds.end_timestamp)
assert.is_not_nil(week_bounds.start_formatted)
assert.is_not_nil(week_bounds.end_formatted)
-- Should be 7 days apart
local duration = week_bounds.end_timestamp - week_bounds.start_timestamp
assert.equals(6 * 86400, duration) -- 6 days in seconds
end)
end)
describe("get_month_bounds", function()
it("should get month start and end", function()
local month_bounds = date_utils.get_month_bounds(os.time({year = 2023, month = 12, day = 15}))
assert.is_not_nil(month_bounds.start_timestamp)
assert.is_not_nil(month_bounds.end_timestamp)
assert.equals("2023-12-01", month_bounds.start_formatted)
assert.equals("2023-12-31", month_bounds.end_formatted)
end)
end)
describe("get_timezones", function()
it("should return list of timezones", function()
local timezones = date_utils.get_timezones()
assert.is_table(timezones)
assert.is_true(#timezones > 0)
assert.is_true(vim.tbl_contains(timezones, "UTC"))
assert.is_true(vim.tbl_contains(timezones, "America/New_York"))
end)
end)
end)

View file

@ -0,0 +1,247 @@
-- Unit tests for type detection and conversion utilities
local types = require('notex.utils.types')
describe("type utilities", function()
describe("detect_type", function()
it("should detect boolean true values", function()
local detected_type, converted_value = types.detect_type("true")
assert.equals("boolean", detected_type)
assert.is_true(converted_value)
detected_type, converted_value = types.detect_type("yes")
assert.equals("boolean", detected_type)
assert.is_true(converted_value)
detected_type, converted_value = types.detect_type("1")
assert.equals("boolean", detected_type)
assert.is_true(converted_value)
end)
it("should detect boolean false values", function()
local detected_type, converted_value = types.detect_type("false")
assert.equals("boolean", detected_type)
assert.is_false(converted_value)
detected_type, converted_value = types.detect_type("no")
assert.equals("boolean", detected_type)
assert.is_false(converted_value)
detected_type, converted_value = types.detect_type("0")
assert.equals("boolean", detected_type)
assert.is_false(converted_value)
end)
it("should detect ISO 8601 dates", function()
local detected_type = types.detect_type("2023-12-25")
assert.equals("date", detected_type)
detected_type = types.detect_type("2023-12-25T10:30:00")
assert.equals("date", detected_type)
end)
it("should detect URLs", function()
local detected_type = types.detect_type("https://example.com")
assert.equals("url", detected_type)
detected_type = types.detect_type("http://test.org/path")
assert.equals("url", detected_type)
end)
it("should detect email addresses", function()
local detected_type = types.detect_type("user@example.com")
assert.equals("email", detected_type)
detected_type = types.detect_type("test.email+tag@domain.co.uk")
assert.equals("email", detected_type)
end)
it("should detect numbers", function()
local detected_type = types.detect_type("42")
assert.equals("number", detected_type)
detected_type = types.detect_type("-17")
assert.equals("number", detected_type)
detected_type = types.detect_type("3.14159")
assert.equals("number", detected_type)
end)
it("should detect JSON arrays", function()
local detected_type = types.detect_type('[1, 2, 3]')
assert.equals("array", detected_type)
detected_type = types.detect_type('["a", "b", "c"]')
assert.equals("array", detected_type)
end)
it("should detect JSON objects", function()
local detected_type = types.detect_type('{"key": "value"}')
assert.equals("object", detected_type)
detected_type = types.detect_type('{"a": 1, "b": 2}')
assert.equals("object", detected_type)
end)
it("should detect strings by default", function()
local detected_type = types.detect_type("plain text")
assert.equals("string", detected_type)
detected_type = types.detect_type("not a special pattern")
assert.equals("string", detected_type)
end)
it("should detect nil values", function()
local detected_type = types.detect_type(nil)
assert.equals("nil", detected_type)
end)
end)
describe("convert_to_type", function()
it("should convert to boolean", function()
assert.is_true(types.convert_to_type("true", "boolean"))
assert.is_false(types.convert_to_type("false", "boolean"))
assert.is_true(types.convert_to_type("yes", "boolean"))
assert.is_false(types.convert_to_type("no", "boolean"))
end)
it("should convert to number", function()
assert.equals(42, types.convert_to_type("42", "number"))
assert.equals(-17.5, types.convert_to_type("-17.5", "number"))
assert.equals(0, types.convert_to_type("invalid", "number"))
end)
it("should convert to string", function()
assert.equals("hello", types.convert_to_type("hello", "string"))
assert.equals("42", types.convert_to_type(42, "string"))
assert.equals("true", types.convert_to_type(true, "string"))
end)
it("should convert to array", function()
local array = types.convert_to_type('[1, 2, 3]', "array")
assert.is_table(array)
assert.equals(1, array[1])
assert.equals(2, array[2])
assert.equals(3, array[3])
-- Comma-separated values
array = types.convert_to_type("a,b,c", "array")
assert.is_table(array)
assert.equals("a", array[1])
assert.equals("b", array[2])
assert.equals("c", array[3])
end)
it("should convert to object", function()
local obj = types.convert_to_type('{"key": "value"}', "object")
assert.is_table(obj)
assert.equals("value", obj.key)
-- Key=value pairs
obj = types.convert_to_type("a=1,b=2", "object")
assert.is_table(obj)
assert.equals("1", obj.a)
assert.equals("2", obj.b)
end)
end)
describe("compare_types", function()
it("should compare types correctly", function()
local result = types.compare_types("hello", 42)
assert.equals("string", result.type1)
assert.equals("number", result.type2)
assert.is_false(result.same_type)
assert.is_true(result.compatible) -- strings can represent numbers
result = types.compare_types(42, 17)
assert.equals("number", result.type1)
assert.equals("number", result.type2)
assert.is_true(result.same_type)
assert.is_true(result.compatible)
end)
end)
describe("are_types_compatible", function()
it("should check type compatibility", function()
assert.is_true(types.are_types_compatible("string", "string"))
assert.is_true(types.are_types_compatible("number", "number"))
assert.is_true(types.are_types_compatible("string", "number")) -- string can convert to number
assert.is_true(types.are_types_compatible("number", "string")) -- string can represent number
assert.is_false(types.are_types_compatible("boolean", "table"))
end)
end)
describe("cast_value", function()
it("should cast values with validation", function()
assert.equals(42, types.cast_value("42", "number"))
assert.equals("hello", types.cast_value("hello", "string"))
-- Invalid cast returns original in non-strict mode
assert.equals("invalid", types.cast_value("invalid", "number"))
end)
it("should error in strict mode", function()
assert.has_error(function()
types.cast_value("invalid", "number", true)
end)
end)
end)
describe("infer_schema", function()
it("should infer schema from values", function()
local values = {"hello", "world", "test"}
local schema = types.infer_schema(values)
assert.equals("string", schema.detected_type)
assert.equals(1.0, schema.confidence)
assert.equals(3, schema.sample_size)
assert.equals(5, schema.constraints.max_length)
assert.equals(4, schema.constraints.min_length)
end)
it("should handle mixed types", function()
local values = {"hello", 42, true}
local schema = types.infer_schema(values)
-- Should pick the most common type
assert.is_not_nil(schema.detected_type)
assert.is_true(schema.confidence < 1.0)
end)
end)
describe("get_possible_conversions", function()
it("should get all possible conversions", function()
local conversions = types.get_possible_conversions("42")
-- Should include number, boolean, string, and possibly date conversions
local found_types = {}
for _, conversion in ipairs(conversions) do
found_types[conversion.type] = true
end
assert.is_true(found_types.number)
assert.is_true(found_types.string)
assert.is_true(found_types.boolean)
end)
end)
describe("validate_conversion", function()
it("should validate type conversion", function()
assert.is_true(types.validate_conversion("42", "number"))
assert.is_true(types.validate_conversion("true", "boolean"))
assert.is_false(types.validate_conversion("invalid", "number"))
end)
end)
describe("get_type_info", function()
it("should get comprehensive type information", function()
local info = types.get_type_info("42")
assert.equals("number", info.detected_type)
assert.equals("42", info.original_value)
assert.equals(42, info.converted_value)
assert.is_table(info.possible_conversions)
end)
end)
end)

View file

@ -0,0 +1,282 @@
-- Unit tests for validation utilities
local validation = require('notex.utils.validation')
describe("validation utilities", function()
describe("validate_value", function()
it("should validate string values", function()
local schema = {type = "string", required = true}
local valid, error = validation.validate_value("hello", schema)
assert.is_true(valid)
assert.equals("Validation passed", error)
end)
it("should reject required string when nil", function()
local schema = {type = "string", required = true}
local valid, error = validation.validate_value(nil, schema)
assert.is_false(valid)
assert.equals("Value is required", error)
end)
it("should validate string length constraints", function()
local schema = {type = "string", min_length = 5, max_length = 10}
-- Too short
local valid, error = validation.validate_value("hi", schema)
assert.is_false(valid)
assert.matches("too short", error)
-- Too long
valid, error = validation.validate_value("this is too long", schema)
assert.is_false(valid)
assert.matches("too long", error)
-- Just right
valid, error = validation.validate_value("perfect", schema)
assert.is_true(valid)
end)
it("should validate number values", function()
local schema = {type = "number", min_value = 1, max_value = 10}
-- Valid number
local valid, error = validation.validate_value(5, schema)
assert.is_true(valid)
-- Too small
valid, error = validation.validate_value(0, schema)
assert.is_false(valid)
assert.matches("too small", error)
-- Too large
valid, error = validation.validate_value(11, schema)
assert.is_false(valid)
assert.matches("too large", error)
end)
it("should validate boolean values", function()
local schema = {type = "boolean"}
-- Valid booleans
local valid, error = validation.validate_value(true, schema)
assert.is_true(valid)
valid, error = validation.validate_value(false, schema)
assert.is_true(valid)
-- String conversion
valid, error = validation.validate_value("true", schema)
assert.is_true(valid)
valid, error = validation.validate_value("false", schema)
assert.is_true(valid)
end)
it("should validate array values", function()
local schema = {type = "array", min_items = 2, max_items = 5}
-- Valid array
local valid, error = validation.validate_value({1, 2, 3}, schema)
assert.is_true(valid)
-- Too few items
valid, error = validation.validate_value({1}, schema)
assert.is_false(valid)
assert.matches("too short", error)
-- Too many items
valid, error = validation.validate_value({1, 2, 3, 4, 5, 6}, schema)
assert.is_false(valid)
assert.matches("too long", error)
end)
it("should validate object values", function()
local schema = {
type = "object",
required_fields = {"name"},
field_types = {
name = {type = "string"},
age = {type = "number"}
}
}
-- Valid object
local valid, error = validation.validate_value({name = "John", age = 30}, schema)
assert.is_true(valid)
-- Missing required field
valid, error = validation.validate_value({age = 30}, schema)
assert.is_false(valid)
assert.matches("Missing required field", error)
-- Invalid field type
valid, error = validation.validate_value({name = 123, age = 30}, schema)
assert.is_false(valid)
assert.matches("Field 'name' invalid", error)
end)
end)
describe("validate_document_properties", function()
it("should validate document properties against schema", function()
local schema_definition = {
title = {type = "string", required = true},
status = {type = "string", enum = {"draft", "published"}},
priority = {type = "number", min_value = 1, max_value = 5}
}
local properties = {
title = "Test Document",
status = "draft",
priority = 3
}
local valid, result = validation.validate_document_properties(properties, schema_definition)
assert.is_true(valid)
assert.equals(0, #result.errors)
end)
it("should return errors for invalid properties", function()
local schema_definition = {
title = {type = "string", required = true},
status = {type = "string", enum = {"draft", "published"}}
}
local properties = {
status = "invalid_status" -- Missing required title, invalid status
}
local valid, result = validation.validate_document_properties(properties, schema_definition)
assert.is_false(valid)
assert.is_true(#result.errors > 0)
end)
it("should include warnings for unknown properties", function()
local schema_definition = {
title = {type = "string", required = true}
}
local properties = {
title = "Test Document",
unknown_property = "value"
}
local valid, result = validation.validate_document_properties(properties, schema_definition)
assert.is_true(valid)
assert.is_true(#result.warnings > 0)
end)
end)
describe("validate_query_params", function()
it("should validate query parameters", function()
local allowed_params = {
limit = {type = "number", min_value = 1, max_value = 100},
sort = {type = "string", enum = {"asc", "desc"}},
filter = {type = "string", required = false}
}
local params = {
limit = 10,
sort = "asc"
}
local valid, errors = validation.validate_query_params(params, allowed_params)
assert.is_true(valid)
assert.equals(0, #errors)
end)
it("should reject unknown parameters", function()
local allowed_params = {
limit = {type = "number"}
}
local params = {
limit = 10,
unknown = "value"
}
local valid, errors = validation.validate_query_params(params, allowed_params)
assert.is_false(valid)
assert.is_true(#errors > 0)
end)
end)
describe("sanitize_input", function()
it("should remove dangerous characters", function()
local input = '<script>alert("xss")</script>'
local sanitized = validation.sanitize_input(input)
assert.equals('scriptalertxss/script', sanitized)
end)
it("should limit length", function()
local input = string.rep("a", 100)
local sanitized = validation.sanitize_input(input, {max_length = 10})
assert.equals(10, #sanitized)
end)
it("should trim whitespace", function()
local input = " hello world "
local sanitized = validation.sanitize_input(input)
assert.equals("hello world", sanitized)
end)
end)
describe("validate_file_path", function()
it("should validate safe file paths", function()
local valid, error = validation.validate_file_path("/home/user/document.md")
assert.is_true(valid)
end)
it("should reject paths with invalid characters", function()
local valid, error = validation.validate_file_path('file<name>.md')
assert.is_false(valid)
assert.matches("Invalid characters", error)
end)
it("should reject directory traversal", function()
local valid, error = validation.validate_file_path("../../../etc/passwd")
assert.is_false(valid)
assert.matches("Directory traversal", error)
end)
it("should reject empty paths", function()
local valid, error = validation.validate_file_path("")
assert.is_false(valid)
assert.equals("Empty file path", error)
end)
end)
describe("create_schema", function()
it("should create validation schema", function()
local schema = validation.create_schema("test_field", {
type = "string",
required = true,
min_length = 5
})
assert.equals("test_field", schema.field_name)
assert.equals("string", schema.type)
assert.is_true(schema.required)
assert.equals(5, schema.min_length)
end)
end)
describe("create_validation_summary", function()
it("should create validation summary", function()
local results = {
{valid = true},
{valid = false, errors = {"Error 1"}},
{valid = false, errors = {"Error 2", "Error 3"}, warnings = {"Warning 1"}},
{valid = true, warnings = {"Warning 2"}}
}
local summary = validation.create_validation_summary(results)
assert.equals(4, summary.total)
assert.equals(2, summary.valid)
assert.equals(2, summary.invalid)
assert.equals(3, #summary.errors)
assert.equals(2, #summary.warnings)
end)
end)
end)