Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 163 additions & 0 deletions spec/unit/bundle_loader_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -609,3 +609,166 @@ runner:then_("^the saas client received a bundle_activated event$", function(ctx
end)

runner:feature_file_relative("features/bundle_loader.feature")

describe("bundle_loader targeted direct branch coverage", function()
local bundle_loader

before_each(function()
mock_ngx.setup_ngx()
bundle_loader = _reload_modules()
end)

it("returns top-level validation errors for invalid bundle shape", function()
local errors = bundle_loader.validate_bundle(nil)
assert.same({ "bundle must be a table" }, errors)
end)

it("rejects invalid policy, rule, fallback_limit, and unsupported algorithm", function()
local compiled, err = bundle_loader.load_from_string(
table.concat({
'{"bundle_version":1,"policies":[',
'{"id":"bad-rule","spec":{"selector":{"pathPrefix":"/a","methods":["GET"]},',
'"rules":[{"limit_keys":["jwt:sub"],"algorithm":"token_bucket",',
'"algorithm_config":{"tokens_per_second":1,"burst":1}}]}},',
'{"id":"bad-fallback","spec":{"selector":{"pathPrefix":"/b","methods":["GET"]},',
'"rules":[{"name":"ok","limit_keys":["jwt:sub"],"algorithm":"token_bucket",',
'"algorithm_config":{"tokens_per_second":1,"burst":1}}],"fallback_limit":"oops"}},',
'{"id":"bad-algo","spec":{"selector":{"pathPrefix":"/c","methods":["GET"]},',
'"rules":[{"name":"weird","limit_keys":["jwt:sub"],"algorithm":"weird",',
'"algorithm_config":{"burst":1}}]}}',
']}',
})
)
assert.is_table(compiled)
assert.is_nil(err)
assert.equals(0, #compiled.policies)
assert.same({
"policy[1] rule[1]: missing name",
"policy=bad-fallback: fallback_limit must be a table",
"policy=bad-algo rule=weird invalid_algorithm_config: unsupported algorithm",
}, compiled.validation_errors)
end)

it("queues audit event for malformed signed bundle payload", function()
local events = {}
bundle_loader.init({
saas_client = {
queue_event = function(event)
events[#events + 1] = event
end,
},
})

local compiled, err = bundle_loader.load_from_string("not-a-signed-bundle", "signing-key")
assert.is_nil(compiled)
assert.equals("signed_bundle_format_error", err)
assert.equals("bundle_rejected", events[1].event_type)
assert.equals("signed_bundle_format_error", events[1].rejection_reason)
end)

it("returns file and apply guard errors", function()
local compiled, load_err = bundle_loader.load_from_file(nil)
assert.is_nil(compiled)
assert.equals("file_path_required", load_err)

local applied, apply_err = bundle_loader.apply(nil)
assert.is_nil(applied)
assert.equals("compiled_bundle_required", apply_err)
end)

it("fails hot reload init when timer registration fails", function()
ngx.timer = {
every = function()
return nil, "boom"
end,
}

local ok, err = bundle_loader.init_hot_reload(5, "/tmp/nope.json")
assert.is_nil(ok)
assert.equals("hot_reload_init_failed: boom", err)
end)

it("returns validation errors for top-level bundle edge cases", function()
assert.same({ "bundle_version must be a positive number" }, bundle_loader.validate_bundle({
bundle_version = 0,
policies = {},
}))

assert.same({ "policies must be a table" }, bundle_loader.validate_bundle({
bundle_version = 1,
policies = false,
}))
end)

it("returns policy validation errors for invalid selector and mode", function()
local errors = bundle_loader.validate_bundle({
bundle_version = 1,
policies = {
{ id = "bad-selector", spec = {} },
{ id = "bad-mode", spec = { selector = {}, mode = "bogus", rules = {} } },
},
})
assert.same({
"policy=bad-selector: missing selector",
"policy=bad-mode: invalid mode",
}, errors)
end)

it("returns policy validation error when policy id is missing", function()
local errors = bundle_loader.validate_bundle({
bundle_version = 1,
policies = {
{ spec = { selector = {}, rules = {} } },
},
})

assert.same({
"policy[1]: missing id",
}, errors)
end)

it("rejects invalid top-level timestamps and malformed policies table", function()
assert.same({ "issued_at_invalid" }, bundle_loader.validate_bundle({
bundle_version = 1,
policies = {},
issued_at = "bad",
}))

assert.same({ "expires_at_invalid" }, bundle_loader.validate_bundle({
bundle_version = 1,
policies = {},
expires_at = "bad",
}))

assert.same({ "policy=bad-rules: rules must be a table" }, bundle_loader.validate_bundle({
bundle_version = 1,
policies = {
{ id = "bad-rules", spec = { selector = {}, rules = false } },
},
}))
end)

it("returns json_string_required for empty payload", function()
local compiled, err = bundle_loader.load_from_string("")
assert.is_nil(compiled)
assert.equals("json_string_required", err)
end)

it("validates cost_based rules and normalizes selector hosts with ports", function()
local compiled, err = bundle_loader.load_from_string(
table.concat({
'{"bundle_version":2,"policies":[',
'{"id":"cost-policy","spec":{"selector":{',
'"pathPrefix":"/cost","methods":["POST"],"hosts":["Example.COM:443"]},',
'"rules":[{"name":"cost-rule","limit_keys":["jwt:sub"],',
'"algorithm":"cost_based","algorithm_config":{"budget":100,"period":"1h",',
'"staged_actions":[{"threshold_percent":100,"action":"reject"}]}}]}}',
']}',
})
)

assert.is_nil(err)
assert.is_table(compiled)
assert.equals("example.com", compiled.policies[1].spec.selector.hosts[1])
end)
end)
20 changes: 20 additions & 0 deletions spec/unit/circuit_breaker_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,23 @@ runner:then_('^current and previous rate keys for limit key "([^"]+)" are cleare
end)

runner:feature_file_relative("features/circuit_breaker.feature")

describe("circuit_breaker targeted direct coverage", function()
it("rejects invalid config shapes and fills defaults", function()
local ok, err = circuit_breaker.validate_config(nil)
assert.is_true(ok)
assert.is_nil(err)

ok, err = circuit_breaker.validate_config({ enabled = "yes" })
assert.is_nil(ok)
assert.equals("circuit_breaker.enabled must be a boolean", err)

local config = { enabled = true, spend_rate_threshold_per_minute = 10 }
ok, err = circuit_breaker.validate_config(config)
assert.is_true(ok)
assert.is_nil(err)
assert.equals("reject", config.action)
assert.equals(0, config.auto_reset_after_minutes)
assert.is_false(config.alert)
end)
end)
88 changes: 88 additions & 0 deletions spec/unit/cost_budget_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,91 @@ runner:then_("^the period key at now ([%d%.]+) stores usage (%d+)$", function(ct
end)

runner:feature_file_relative("features/cost_budget.feature")

describe("cost_budget targeted direct coverage", function()
it("rejects invalid now and budget inputs for compute_period_start", function()
local period_start, err = cost_budget.compute_period_start("minute", nil)
assert.is_nil(period_start)
assert.equals("now must be a number", err)

period_start, err = cost_budget.compute_period_start("bogus", 10)
assert.is_nil(period_start)
assert.equals("unknown period", err)
end)

it("fills config defaults and rejects invalid cost configuration", function()
local ok, err = cost_budget.validate_config({
algorithm = "cost_based",
budget = 100,
period = "1h",
cost_key = "bogus",
})
assert.is_nil(ok)
assert.equals("cost_key must be fixed, header:<name>, or query:<name>", err)

local config = {
algorithm = "cost_based",
budget = 100,
period = "1h",
staged_actions = {
{ threshold_percent = 100, action = "reject" },
},
}
ok, err = cost_budget.validate_config(config)
assert.is_true(ok)
assert.is_nil(err)
assert.equals("fixed", config.cost_key)
assert.equals(1, config.default_cost)
assert.equals(1, config.fixed_cost)
end)

it("rejects invalid budget, default_cost, and staged_actions entries", function()
local ok, err = cost_budget.validate_config({
algorithm = "cost_based",
budget = 0,
period = "1h",
staged_actions = {
{ threshold_percent = 100, action = "reject" },
},
})
assert.is_nil(ok)
assert.equals("budget must be a positive number", err)

ok, err = cost_budget.validate_config({
algorithm = "cost_based",
budget = 10,
period = "1h",
default_cost = 0,
staged_actions = {
{ threshold_percent = 100, action = "reject" },
},
})
assert.is_nil(ok)
assert.equals("default_cost must be a positive number", err)

ok, err = cost_budget.validate_config({
algorithm = "cost_based",
budget = 10,
period = "1h",
staged_actions = { "bad" },
})
assert.is_nil(ok)
assert.equals("staged_action must be a table", err)
end)

it("falls back to default cost for invalid sources", function()
local cost = cost_budget.resolve_cost({
_cost_key_kind = "header",
_cost_key_name = "x-cost",
default_cost = 7,
}, { headers = {} })
assert.equals(7, cost)

cost = cost_budget.resolve_cost({
_cost_key_kind = "fixed",
default_cost = 9,
fixed_cost = 0,
}, {})
assert.equals(9, cost)
end)
end)
40 changes: 40 additions & 0 deletions spec/unit/cost_extractor_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,43 @@ runner:then_('^reconcile fails with error "([^"]+)"$', function(ctx, expected)
end)

runner:feature_file_relative("features/cost_extractor.feature")

describe("cost_extractor targeted direct coverage", function()
it("rejects invalid config bounds and fallback", function()
local ok, err = cost_extractor.validate_config({ max_parseable_body_bytes = 0 })
assert.is_nil(ok)
assert.equals("max_parseable_body_bytes must be > 0", err)

ok, err = cost_extractor.validate_config({ fallback = "" })
assert.is_nil(ok)
assert.equals("fallback must be a non-empty string", err)
end)

it("returns fallback errors for malformed JSON body", function()
local result, err, meta = cost_extractor.extract_from_response("{bad json", {
fallback = "default_cost",
max_parseable_body_bytes = 1024,
max_parse_time_ms = 100,
})
assert.is_nil(result)
assert.equals("json_parse_error", err)
assert.is_true(meta.fallback)
end)

it("rejects invalid stream buffer and parse time limits", function()
local ok, err = cost_extractor.validate_config({ max_stream_buffer_bytes = 0 })
assert.is_nil(ok)
assert.equals("max_stream_buffer_bytes must be > 0", err)

ok, err = cost_extractor.validate_config({ max_parse_time_ms = 0 })
assert.is_nil(ok)
assert.equals("max_parse_time_ms must be > 0", err)
end)

it("returns config_invalid when config is missing", function()
local result, err, meta = cost_extractor.extract_from_response("{}", nil)
assert.is_nil(result)
assert.equals("config_invalid", err)
assert.is_true(meta.fallback)
end)
end)
Loading
Loading