Restructure Klaviyo detector pattern tests (#5026)

Rewrite the single table-driven pattern test as one focused test per
case, and add the coverage the tightened regex needs.

* Legacy lowercase-hex and the newer prefixed company_id format each get
  their own positive test.
* New negatives: uppercase-hex body, a legacy body one char short, and a
  new-format hex tail one char long.
* Inline each test's input and assertions; drop the shared fixture block.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Martin Locklear
2026-06-09 14:55:58 -04:00
committed by GitHub
co-authored by Cursor
parent cc90719407
commit 28f515fbb9
+66 -68
View File
@@ -1,3 +1,4 @@
// Pattern tests for the Klaviyo detector.
package klaviyo
import (
@@ -5,83 +6,80 @@ import (
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/suite"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
)
var (
validPattern = "pk_a1b2c3d4e5f60718293a4b5c6d7e8f9012"
validPatternNew = "pk_AbCdEf_f0e1d2c3b4a5968778695a4b3c2d1e0f00"
invalidPattern = "pk_1234567890abcdefghijklmnopqrstu-_="
keyword = "klaviyo"
)
// KlaviyoPatternSuite drives keyPat through the engine's real path (keyword
// prefilter, then FromData). One method per case, so a failure names it.
type KlaviyoPatternSuite struct {
suite.Suite
d Scanner
core *ahocorasick.Core
}
func TestKlaviyo_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
tests := []struct {
name string
input string
want []string
}{
{
name: "valid pattern - with keyword klaviyo",
input: fmt.Sprintf("%s token = '%s'", keyword, validPattern),
want: []string{validPattern},
},
{
name: "valid pattern (new format) - with keyword klaviyo",
input: fmt.Sprintf("%s token = '%s'", keyword, validPatternNew),
want: []string{validPatternNew},
},
{
name: "invalid pattern",
input: fmt.Sprintf("%s = '%s'", keyword, invalidPattern),
want: []string{},
},
}
func TestKlaviyoPatternSuite(t *testing.T) {
suite.Run(t, new(KlaviyoPatternSuite))
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(matchedDetectors) == 0 {
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
return
}
// Fresh scanner and prefilter per method, so state can't leak between cases.
func (s *KlaviyoPatternSuite) SetupTest() {
s.d = Scanner{}
s.core = ahocorasick.NewAhoCorasickCore([]detectors.Detector{s.d})
}
results, err := d.FromData(context.Background(), false, []byte(test.input))
if err != nil {
t.Errorf("error = %v", err)
return
}
// Legacy keys must keep matching despite the tighter lowercase-hex rule.
func (s *KlaviyoPatternSuite) TestLegacyHexFormat() {
key := "pk_a1b2c3d4e5f60718293a4b5c6d7e8f9012" // pk_ + 34 lowercase hex
input := fmt.Sprintf("klaviyo token = '%s'", key)
s.Require().NotEmpty(s.core.FindDetectorMatches([]byte(input)), "keyword prefilter should fire")
if len(results) != len(test.want) {
if len(results) == 0 {
t.Errorf("did not receive result")
} else {
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
}
return
}
results, err := s.d.FromData(context.Background(), false, []byte(input))
s.Require().NoError(err)
s.Require().Len(results, 1)
s.Equal(key, string(results[0].Raw))
}
actual := make(map[string]struct{}, len(results))
for _, r := range results {
if len(r.RawV2) > 0 {
actual[string(r.RawV2)] = struct{}{}
} else {
actual[string(r.Raw)] = struct{}{}
}
}
expected := make(map[string]struct{}, len(test.want))
for _, v := range test.want {
expected[v] = struct{}{}
}
// The newer prefixed format. Mixed-case company_id is the bit a hex-only
// guess would miss.
func (s *KlaviyoPatternSuite) TestNewPrefixedFormat() {
key := "pk_AbCdEf_f0e1d2c3b4a5968778695a4b3c2d1e0f00" // pk_ + 6 alnum company_id + _ + 34 hex
input := fmt.Sprintf("klaviyo token = '%s'", key)
s.Require().NotEmpty(s.core.FindDetectorMatches([]byte(input)), "keyword prefilter should fire")
if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
}
})
}
results, err := s.d.FromData(context.Background(), false, []byte(input))
s.Require().NoError(err)
s.Require().Len(results, 1)
s.Equal(key, string(results[0].Raw))
}
// Punctuation in the body should never read as a key.
func (s *KlaviyoPatternSuite) TestPunctuationBodyRejected() {
results, err := s.d.FromData(context.Background(), false, []byte("klaviyo = 'pk_1234567890abcdefghijklmnopqrstu-_='"))
s.Require().NoError(err)
s.Empty(results)
}
// Legacy bodies are lowercase hex (vendor-confirmed), so uppercase is a false
// positive.
func (s *KlaviyoPatternSuite) TestUppercaseHexRejected() {
results, err := s.d.FromData(context.Background(), false, []byte("klaviyo = 'pk_A1B2C3D4E5F60718293A4B5C6D7E8F9012'"))
s.Require().NoError(err)
s.Empty(results)
}
// Guards the legacy {34} length.
func (s *KlaviyoPatternSuite) TestLegacyTooShortRejected() {
results, err := s.d.FromData(context.Background(), false, []byte("klaviyo = 'pk_a1b2c3d4e5f60718293a4b5c6d7e8f901'")) // 33 hex
s.Require().NoError(err)
s.Empty(results)
}
// Guards the new-format {34} tail and trailing \b.
func (s *KlaviyoPatternSuite) TestNewFormatTailTooLongRejected() {
results, err := s.d.FromData(context.Background(), false, []byte("klaviyo = 'pk_AbCdEf_f0e1d2c3b4a5968778695a4b3c2d1e0f001'")) // 35 hex tail
s.Require().NoError(err)
s.Empty(results)
}