Initial vibecoded proof of concept
This commit is contained in:
parent
74812459af
commit
461318a656
61 changed files with 13306 additions and 0 deletions
324
tests/unit/utils/test_cache.lua
Normal file
324
tests/unit/utils/test_cache.lua
Normal 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)
|
277
tests/unit/utils/test_date.lua
Normal file
277
tests/unit/utils/test_date.lua
Normal 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)
|
247
tests/unit/utils/test_types.lua
Normal file
247
tests/unit/utils/test_types.lua
Normal 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)
|
282
tests/unit/utils/test_validation.lua
Normal file
282
tests/unit/utils/test_validation.lua
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue