Skip to content

Commit 67928fe

Browse files
committed
docs+ci: update TODO (Terraform scope, coverage target); add coverage threshold check (>=80%) in CI; add ParamFilters and HashUtils unit tests; expand Terraform docs with recipes and provider link
1 parent cf2e871 commit 67928fe

File tree

5 files changed

+177
-127
lines changed

5 files changed

+177
-127
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ jobs:
163163
- name: Merge coverage reports
164164
run: scripts/merge_coverage.sh
165165

166+
- name: Enforce coverage threshold (>= 80%)
167+
run: ruby scripts/check_coverage_threshold.rb
168+
166169
# Save merged coverage data as an artifact for deploy workflow
167170
- name: Upload coverage reports
168171
uses: actions/upload-artifact@v4
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env ruby
2+
# typed: strict
3+
# frozen_string_literal: true
4+
5+
require "json"
6+
7+
threshold = (ENV["COVERAGE_MIN"] || "80").to_f
8+
coverage_path = File.expand_path(File.join(__dir__, "..", "site/public/coverage/coverage.json"))
9+
10+
unless File.exist?(coverage_path)
11+
warn "Coverage file not found at #{coverage_path}. Did you run merge_coverage.sh?"
12+
exit 1
13+
end
14+
15+
data = JSON.parse(File.read(coverage_path))
16+
percent = data.dig("metrics", "covered_percent").to_f
17+
18+
puts "Total coverage: #{format("%.2f", percent)}% (threshold: #{threshold}%)"
19+
20+
if percent < threshold
21+
warn "Coverage threshold not met: #{percent}% < #{threshold}%"
22+
exit 1
23+
end
24+
25+
exit 0

site/app/docs/terraform/page.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,72 @@ export default function TerraformDocsPage() {
108108
</ul>
109109
</section>
110110

111+
<section id="recipes" className="space-y-4">
112+
<h2 className="text-2xl font-semibold">Recipes</h2>
113+
<p className="text-neutral-600 dark:text-neutral-400">
114+
A few helpful patterns you can copy and adapt:
115+
</p>
116+
<div className="grid gap-6 md:grid-cols-2">
117+
<CodeBlock language="hcl" title="Count Email Deliveries">
118+
{`data "logstruct_cloudwatch_filter" "email_delivered" {
119+
struct = "ActionMailer"
120+
event = "delivered"
121+
}
122+
123+
resource "aws_cloudwatch_log_metric_filter" "email_delivered_count" {
124+
name = "Email Delivered Count"
125+
log_group_name = var.log_group.app
126+
pattern = data.logstruct_cloudwatch_filter.email_delivered.pattern
127+
128+
metric_transformation {
129+
name = "app_email_delivered_count"
130+
namespace = var.namespace.logs
131+
value = "1"
132+
unit = "Count"
133+
}
134+
}`}
135+
</CodeBlock>
136+
137+
<CodeBlock language="hcl" title="Count Successful GoodJob Runs">
138+
{`data "logstruct_cloudwatch_filter" "goodjob_finish" {
139+
struct = "GoodJob"
140+
event = "finish"
141+
}
142+
143+
resource "aws_cloudwatch_log_metric_filter" "goodjob_finish_count" {
144+
name = "GoodJob Finish Count"
145+
log_group_name = var.log_group.app
146+
pattern = data.logstruct_cloudwatch_filter.goodjob_finish.pattern
147+
148+
metric_transformation {
149+
name = "app_goodjob_finish_count"
150+
namespace = var.namespace.logs
151+
value = "1"
152+
unit = "Count"
153+
}
154+
}`}
155+
</CodeBlock>
156+
</div>
157+
<p className="text-neutral-600 dark:text-neutral-400">
158+
See the provider README for more examples and details.
159+
</p>
160+
</section>
161+
162+
<section id="links" className="space-y-2">
163+
<h2 className="text-2xl font-semibold">Links</h2>
164+
<ul className="list-disc pl-6">
165+
<li>
166+
<a
167+
href="https://github.com/DocSpring/terraform-provider-logstruct"
168+
target="_blank"
169+
rel="noopener noreferrer"
170+
>
171+
Provider README (GitHub)
172+
</a>
173+
</li>
174+
</ul>
175+
</section>
176+
111177
<EditPageLink path="app/docs/terraform/page.tsx" />
112178
</div>
113179
);

test/log_struct/hash_utils_test.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# typed: true
2+
# frozen_string_literal: true
3+
4+
require "test_helper"
5+
6+
module LogStruct
7+
class HashUtilsTest < ActiveSupport::TestCase
8+
test "hash_value returns deterministic truncated SHA256" do
9+
# Configure a known salt/length for deterministic output
10+
LogStruct.configure do |c|
11+
c.filters.hash_salt = "salt:"
12+
c.filters.hash_length = 8
13+
end
14+
15+
h1 = HashUtils.hash_value("secret")
16+
h2 = HashUtils.hash_value("secret")
17+
18+
refute_empty h1
19+
assert_equal 8, h1.length
20+
assert_equal h1, h2, "hashing must be deterministic for same input"
21+
22+
# Different input -> different hash
23+
h3 = HashUtils.hash_value("different")
24+
25+
refute_equal h1, h3
26+
end
27+
end
28+
end

test/log_struct/param_filters_test.rb

Lines changed: 55 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -4,168 +4,96 @@
44
require "test_helper"
55

66
module LogStruct
7-
class ParamFiltersTest < Minitest::Test
8-
def setup
9-
# Save original configuration to restore after tests
10-
@original_filter_keys = LogStruct.config.filters.filter_keys.dup
11-
@original_filter_keys_with_hashes = LogStruct.config.filters.filter_keys_with_hashes.dup
12-
13-
# Configure filter keys for testing
14-
LogStruct.config.filters.filter_keys = [:password, :secret, :token]
15-
LogStruct.config.filters.filter_keys_with_hashes = [:email]
7+
class ParamFiltersTest < ActiveSupport::TestCase
8+
setup do
9+
LogStruct.configure do |c|
10+
c.filters.filter_keys = %i[password token]
11+
c.filters.filter_keys_with_hashes = %i[email]
12+
c.filters.hash_salt = "s:"
13+
c.filters.hash_length = 6
14+
end
1615
end
1716

18-
def teardown
19-
# Restore original configuration
20-
LogStruct.config.filters.filter_keys = @original_filter_keys
21-
LogStruct.config.filters.filter_keys_with_hashes = @original_filter_keys_with_hashes
22-
end
23-
24-
def test_should_filter_key
17+
test "should_filter_key? respects configured sensitive keys" do
2518
assert ParamFilters.should_filter_key?(:password)
26-
assert ParamFilters.should_filter_key?("PASSWORD")
27-
assert ParamFilters.should_filter_key?("secret")
28-
assert ParamFilters.should_filter_key?(:token)
29-
19+
assert ParamFilters.should_filter_key?("TOKEN")
3020
refute ParamFilters.should_filter_key?(:username)
31-
refute ParamFilters.should_filter_key?("email")
3221
end
3322

34-
def test_should_include_string_hash
23+
test "should_include_string_hash? respects configured hash keys" do
3524
assert ParamFilters.should_include_string_hash?(:email)
36-
assert ParamFilters.should_include_string_hash?("EMAIL")
37-
3825
refute ParamFilters.should_include_string_hash?(:password)
39-
refute ParamFilters.should_include_string_hash?("username")
4026
end
4127

42-
def test_summarize_string
43-
string = "test-string"
44-
45-
# Without hash
46-
result = ParamFilters.summarize_string(string, false)
47-
48-
assert_equal String, result[:_class]
49-
assert_equal string.bytesize, result[:_bytes]
50-
refute result.key?(:_hash)
51-
52-
# With hash
53-
result = ParamFilters.summarize_string(string, true)
28+
test "summarize_string includes hash when requested" do
29+
s = ParamFilters.summarize_string("abc@ex.com", true)
5430

55-
assert_equal String, result[:_class]
56-
refute result.key?(:_bytes)
57-
assert result.key?(:_hash)
58-
assert_instance_of String, result[:_hash]
31+
assert_equal String, s[:_class]
32+
assert_match(/[0-9a-f]{6}/, s[:_hash])
33+
refute s.key?(:_bytes)
5934
end
6035

61-
def test_summarize_hash_without_sensitive_keys
62-
hash = {name: "John", age: 30}
36+
test "summarize_string includes bytes when not hashing" do
37+
s = ParamFilters.summarize_string("hello", false)
6338

64-
result = ParamFilters.summarize_hash(hash)
65-
66-
assert_equal Hash, result[:_class]
67-
assert_equal 2, result[:_keys_count]
68-
assert_equal [:name, :age], result[:_keys]
69-
assert result.key?(:_bytes)
39+
assert_equal String, s[:_class]
40+
assert_equal 5, s[:_bytes]
41+
refute s.key?(:_hash)
7042
end
7143

72-
def test_summarize_hash_with_sensitive_keys
73-
hash = {name: "John", password: "secret123"}
44+
test "summarize_hash includes keys and optional bytes when no sensitive keys" do
45+
summary = ParamFilters.summarize_hash({a: 1, b: 2})
7446

75-
result = ParamFilters.summarize_hash(hash)
47+
assert_equal Hash, summary[:_class]
48+
assert_equal 2, summary[:_keys_count]
49+
assert_equal [:a, :b], summary[:_keys]
50+
assert_kind_of Integer, summary[:_bytes]
7651

77-
assert_equal Hash, result[:_class]
78-
assert_equal 2, result[:_keys_count]
79-
assert_equal [:name, :password], result[:_keys]
52+
# Presence of sensitive key removes _bytes
53+
summary2 = ParamFilters.summarize_hash({password: "x", a: 1})
8054

81-
# Should not include byte size for hashes with sensitive keys
82-
refute result.key?(:_bytes)
55+
assert_equal Hash, summary2[:_class]
56+
refute summary2.key?(:_bytes)
8357
end
8458

85-
def test_summarize_hash_with_uppercase_sensitive_keys
86-
hash = {name: "John", PASSWORD: "secret123"}
87-
88-
result = ParamFilters.summarize_hash(hash)
89-
90-
assert_equal Hash, result[:_class]
91-
assert_equal 2, result[:_keys_count]
92-
assert_equal [:name, :PASSWORD], result[:_keys]
59+
test "summarize_array reports count/bytes and handles empty" do
60+
s = ParamFilters.summarize_array([1, 2, 3])
9361

94-
# Should not include byte size regardless of case
95-
refute result.key?(:_bytes)
96-
end
62+
assert_equal Array, s[:_class]
63+
assert_equal 3, s[:_count]
64+
assert_operator s[:_bytes], :>, 0
9765

98-
def test_summarize_hash_empty
99-
result = ParamFilters.summarize_hash({})
66+
empty = ParamFilters.summarize_array([])
10067

101-
assert_equal "Hash", result[:_class]
102-
assert result[:_empty]
68+
assert_equal "Array", empty[:_class]
69+
assert empty[:_empty]
10370
end
10471

105-
def test_summarize_array
106-
array = [1, 2, 3]
72+
test "summarize_json_attribute dispatches by type and honors hash keys setting" do
73+
# String + key with hash
74+
s1 = ParamFilters.summarize_json_attribute(:email, "test@x.com")
10775

108-
result = ParamFilters.summarize_array(array)
76+
assert s1.key?(:_hash)
10977

110-
assert_equal Array, result[:_class]
111-
assert_equal 3, result[:_count]
112-
assert result.key?(:_bytes)
113-
end
78+
# String + normal key -> bytes
79+
s2 = ParamFilters.summarize_json_attribute(:message, "hello")
11480

115-
def test_summarize_array_empty
116-
result = ParamFilters.summarize_array([])
81+
assert s2.key?(:_bytes)
11782

118-
assert_equal "Array", result[:_class]
119-
assert result[:_empty]
120-
end
83+
# Hash
84+
s3 = ParamFilters.summarize_json_attribute(:payload, {a: 1})
12185

122-
def test_summarize_json_attribute_with_string
123-
result = ParamFilters.summarize_json_attribute("username", "john")
124-
125-
assert_equal String, result[:_class]
126-
assert_equal "john".bytesize, result[:_bytes]
127-
refute result.key?(:_hash)
128-
129-
result = ParamFilters.summarize_json_attribute("email", "john@example.com")
130-
131-
assert_equal String, result[:_class]
132-
refute result.key?(:_bytes)
133-
assert result.key?(:_hash)
134-
end
135-
136-
def test_summarize_json_attribute_with_hash
137-
hash = {name: "John", age: 30}
138-
result = ParamFilters.summarize_json_attribute("user", hash)
139-
140-
assert_equal Hash, result[:_class]
141-
assert_equal 2, result[:_keys_count]
142-
assert result.key?(:_bytes)
143-
144-
hash_with_sensitive = {name: "John", password: "secret"}
145-
result = ParamFilters.summarize_json_attribute("user", hash_with_sensitive)
146-
147-
assert_equal Hash, result[:_class]
148-
assert_equal 2, result[:_keys_count]
149-
refute result.key?(:_bytes)
150-
end
151-
152-
def test_summarize_json_attribute_with_array
153-
array = [1, 2, 3]
154-
result = ParamFilters.summarize_json_attribute("numbers", array)
155-
156-
assert_equal Array, result[:_class]
157-
assert_equal 3, result[:_count]
158-
assert result.key?(:_bytes)
159-
end
86+
assert_equal Hash, s3[:_class]
16087

161-
def test_summarize_json_attribute_with_other_types
162-
result = ParamFilters.summarize_json_attribute("age", 30)
88+
# Array
89+
s4 = ParamFilters.summarize_json_attribute(:list, [1, 2])
16390

164-
assert_equal Integer, result[:_class]
91+
assert_equal Array, s4[:_class]
16592

166-
result = ParamFilters.summarize_json_attribute("active", true)
93+
# Other
94+
s5 = ParamFilters.summarize_json_attribute(:id, 123)
16795

168-
assert_equal TrueClass, result[:_class]
96+
assert_equal Integer, s5[:_class]
16997
end
17098
end
17199
end

0 commit comments

Comments
 (0)