diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/aws.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/aws.go new file mode 100644 index 00000000..c48b467b --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/aws.go @@ -0,0 +1,48 @@ +// ©AngelaMos | 2026 +// aws.go + +package recipes + +const ( + awsAccessKeyPrefix = "AKIA" + awsAccessKeyBodyLen = 16 + awsSecretBytes = 30 + + awsBucketName = "prod-data-backups" +) + +var awsRegions = []string{ + "us-east-1", + "us-east-2", + "us-west-2", + "eu-west-1", + "eu-central-1", + "ap-southeast-1", + "ap-northeast-1", +} + +type AWS struct{} + +func (AWS) Name() string { return keyAWS } + +func (AWS) Generate() []EnvLine { + return []EnvLine{ + {Comment: "AWS S3 production bucket (" + awsBucketName + ")"}, + { + Key: "AWS_ACCESS_KEY_ID", + Value: awsAccessKeyPrefix + RandomAlnumUpper(awsAccessKeyBodyLen), + }, + { + Key: "AWS_SECRET_ACCESS_KEY", + Value: RandomBase64(awsSecretBytes), + }, + { + Key: "AWS_REGION", + Value: RandomChoice(awsRegions), + }, + { + Key: "AWS_S3_BUCKET", + Value: awsBucketName, + }, + } +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/aws_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/aws_test.go new file mode 100644 index 00000000..7e4a076c --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/aws_test.go @@ -0,0 +1,110 @@ +// ©AngelaMos | 2026 +// aws_test.go + +package recipes_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile/recipes" +) + +func linesByKey( + t *testing.T, + lines []recipes.EnvLine, +) map[string]recipes.EnvLine { + t.Helper() + out := make(map[string]recipes.EnvLine, len(lines)) + for _, l := range lines { + if l.Key != "" { + out[l.Key] = l + } + } + return out +} + +func TestAWSRecipe_Name(t *testing.T) { + require.Equal(t, "aws", recipes.AWS{}.Name()) +} + +func TestAWSRecipe_GeneratesExpectedKeys(t *testing.T) { + lines := recipes.AWS{}.Generate() + byKey := linesByKey(t, lines) + + for _, key := range []string{ + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_REGION", + "AWS_S3_BUCKET", + } { + _, ok := byKey[key] + require.True(t, ok, "AWS recipe must emit %s", key) + } +} + +func TestAWSRecipe_AccessKeyMatchesAKIAFormat(t *testing.T) { + lines := recipes.AWS{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^AKIA[A-Z0-9]{16}$`, + byKey["AWS_ACCESS_KEY_ID"].Value, + "AWS_ACCESS_KEY_ID must match the AKIA + 16 base32-upper-alnum format gitleaks expects", + ) +} + +func TestAWSRecipe_SecretAccessKeyIsBase64Like(t *testing.T) { + lines := recipes.AWS{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^[A-Za-z0-9+/]+={0,2}$`, + byKey["AWS_SECRET_ACCESS_KEY"].Value, + ) + require.GreaterOrEqual( + t, + len(byKey["AWS_SECRET_ACCESS_KEY"].Value), + 40, + "AWS_SECRET_ACCESS_KEY must be at least 40 chars to match the real-key length floor", + ) +} + +func TestAWSRecipe_RegionIsAValidAWSRegion(t *testing.T) { + lines := recipes.AWS{}.Generate() + byKey := linesByKey(t, lines) + region := byKey["AWS_REGION"].Value + require.Regexp( + t, + `^[a-z]{2}-[a-z]+-\d+$`, + region, + "AWS_REGION must match the canonical AWS region pattern (xx-name-n)", + ) +} + +func TestAWSRecipe_HasLeadingComment(t *testing.T) { + lines := recipes.AWS{}.Generate() + require.NotEmpty(t, lines) + require.NotEmpty( + t, + lines[0].Comment, + "AWS recipe must begin with a comment for bait realism", + ) + require.Empty(t, lines[0].Key, "comment line must not carry a Key/Value") +} + +func TestAWSRecipe_DistinctInvocationsProduceDistinctSecrets(t *testing.T) { + seen := make(map[string]struct{}) + for range 20 { + lines := recipes.AWS{}.Generate() + byKey := linesByKey(t, lines) + seen[byKey["AWS_ACCESS_KEY_ID"].Value] = struct{}{} + } + require.Greater( + t, + len(seen), + 18, + "20 invocations should produce near-20 distinct access keys", + ) +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/db.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/db.go new file mode 100644 index 00000000..81ff86f5 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/db.go @@ -0,0 +1,56 @@ +// ©AngelaMos | 2026 +// db.go + +package recipes + +import "fmt" + +const ( + dbPostgresUser = "app_writer" + dbPostgresHost = "db.internal" + dbPostgresPort = 5432 + dbPostgresName = "app_prod" + + dbRedisUser = "default" + dbRedisHost = "cache-prod.internal" + dbRedisPort = 6379 + dbRedisDB = 0 + + dbPostgresPassLen = 24 + dbRedisPassLen = 32 + + dbPostgresURLFmt = "postgres://%s:%s@%s:%d/%s?sslmode=require" + dbRedisURLFmt = "redis://%s:%s@%s:%d/%d" +) + +type DB struct{} + +func (DB) Name() string { return keyDB } + +func (DB) Generate() []EnvLine { + pgPass := RandomAlnumMixed(dbPostgresPassLen) + redisPass := RandomAlnumMixed(dbRedisPassLen) + + pgURL := fmt.Sprintf( + dbPostgresURLFmt, + dbPostgresUser, + pgPass, + dbPostgresHost, + dbPostgresPort, + dbPostgresName, + ) + redisURL := fmt.Sprintf( + dbRedisURLFmt, + dbRedisUser, + redisPass, + dbRedisHost, + dbRedisPort, + dbRedisDB, + ) + + return []EnvLine{ + {Comment: "Primary datastore + cache"}, + {Key: "DATABASE_URL", Value: pgURL}, + {Key: "REDIS_URL", Value: redisURL}, + } +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/db_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/db_test.go new file mode 100644 index 00000000..2bbdbb7d --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/db_test.go @@ -0,0 +1,75 @@ +// ©AngelaMos | 2026 +// db_test.go + +package recipes_test + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile/recipes" +) + +func TestDBRecipe_Name(t *testing.T) { + require.Equal(t, "db", recipes.DB{}.Name()) +} + +func TestDBRecipe_GeneratesExpectedKeys(t *testing.T) { + lines := recipes.DB{}.Generate() + byKey := linesByKey(t, lines) + + for _, key := range []string{"DATABASE_URL", "REDIS_URL"} { + _, ok := byKey[key] + require.True(t, ok, "DB recipe must emit %s", key) + } +} + +func TestDBRecipe_DatabaseURLParsesAsPostgres(t *testing.T) { + lines := recipes.DB{}.Generate() + byKey := linesByKey(t, lines) + parsed, err := url.Parse(byKey["DATABASE_URL"].Value) + require.NoError(t, err) + require.Equal(t, "postgres", parsed.Scheme) + require.Equal(t, "db.internal:5432", parsed.Host) + require.Equal(t, "/app_prod", parsed.Path) + require.Equal(t, "app_writer", parsed.User.Username()) + + pw, ok := parsed.User.Password() + require.True(t, ok, "DATABASE_URL must carry a password") + require.Regexp(t, `^[A-Za-z0-9]{24}$`, pw) + require.Equal(t, "require", parsed.Query().Get("sslmode")) +} + +func TestDBRecipe_RedisURLParsesAsRedis(t *testing.T) { + lines := recipes.DB{}.Generate() + byKey := linesByKey(t, lines) + parsed, err := url.Parse(byKey["REDIS_URL"].Value) + require.NoError(t, err) + require.Equal(t, "redis", parsed.Scheme) + require.Equal(t, "cache-prod.internal:6379", parsed.Host) + require.Equal(t, "/0", parsed.Path) + require.Equal(t, "default", parsed.User.Username()) + + pw, ok := parsed.User.Password() + require.True(t, ok, "REDIS_URL must carry a password") + require.Regexp(t, `^[A-Za-z0-9]{32}$`, pw) +} + +func TestDBRecipe_HasLeadingComment(t *testing.T) { + lines := recipes.DB{}.Generate() + require.NotEmpty(t, lines) + require.NotEmpty(t, lines[0].Comment) + require.Empty(t, lines[0].Key) +} + +func TestDBRecipe_DistinctInvocationsProduceDistinctPasswords(t *testing.T) { + seen := make(map[string]struct{}) + for range 20 { + lines := recipes.DB{}.Generate() + byKey := linesByKey(t, lines) + seen[byKey["DATABASE_URL"].Value] = struct{}{} + } + require.Greater(t, len(seen), 18) +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/github.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/github.go new file mode 100644 index 00000000..2bcf2574 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/github.go @@ -0,0 +1,42 @@ +// ©AngelaMos | 2026 +// github.go + +package recipes + +const ( + githubTokenPrefix = "ghp_" + githubTokenBodyLen = 36 + githubChecksumLen = 6 + githubDeployKeyBytes = 32 + + githubOwnerName = "acme-corp" + githubRepoName = "internal-platform" +) + +type GitHub struct{} + +func (GitHub) Name() string { return keyGitHub } + +func (GitHub) Generate() []EnvLine { + body := RandomAlnumMixed(githubTokenBodyLen) + checksum := RandomAlnumMixed(githubChecksumLen) + return []EnvLine{ + {Comment: "GitHub deploy + automation tokens"}, + { + Key: "GITHUB_TOKEN", + Value: githubTokenPrefix + body + checksum, + }, + { + Key: "GITHUB_DEPLOY_KEY", + Value: RandomBase64(githubDeployKeyBytes), + }, + { + Key: "GITHUB_OWNER", + Value: githubOwnerName, + }, + { + Key: "GITHUB_REPO", + Value: githubRepoName, + }, + } +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/github_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/github_test.go new file mode 100644 index 00000000..7199a857 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/github_test.go @@ -0,0 +1,66 @@ +// ©AngelaMos | 2026 +// github_test.go + +package recipes_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile/recipes" +) + +func TestGitHubRecipe_Name(t *testing.T) { + require.Equal(t, "github", recipes.GitHub{}.Name()) +} + +func TestGitHubRecipe_GeneratesExpectedKeys(t *testing.T) { + lines := recipes.GitHub{}.Generate() + byKey := linesByKey(t, lines) + + for _, key := range []string{ + "GITHUB_TOKEN", + "GITHUB_DEPLOY_KEY", + "GITHUB_OWNER", + "GITHUB_REPO", + } { + _, ok := byKey[key] + require.True(t, ok, "GitHub recipe must emit %s", key) + } +} + +func TestGitHubRecipe_TokenMatchesGhpFormat(t *testing.T) { + lines := recipes.GitHub{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^ghp_[A-Za-z0-9]{42}$`, + byKey["GITHUB_TOKEN"].Value, + "GITHUB_TOKEN must match ghp_ + 36 base62 body + 6 base62 checksum = 42 trailing chars", + ) +} + +func TestGitHubRecipe_DeployKeyIsBase64Like(t *testing.T) { + lines := recipes.GitHub{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^[A-Za-z0-9+/]+={0,2}$`, + byKey["GITHUB_DEPLOY_KEY"].Value, + ) +} + +func TestGitHubRecipe_OwnerAndRepoAreStable(t *testing.T) { + lines := recipes.GitHub{}.Generate() + byKey := linesByKey(t, lines) + require.Equal(t, "acme-corp", byKey["GITHUB_OWNER"].Value) + require.Equal(t, "internal-platform", byKey["GITHUB_REPO"].Value) +} + +func TestGitHubRecipe_HasLeadingComment(t *testing.T) { + lines := recipes.GitHub{}.Generate() + require.NotEmpty(t, lines) + require.NotEmpty(t, lines[0].Comment) + require.Empty(t, lines[0].Key) +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/recipes.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/recipes.go new file mode 100644 index 00000000..a37cb0d5 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/recipes.go @@ -0,0 +1,105 @@ +// ©AngelaMos | 2026 +// recipes.go + +package recipes + +import ( + "crypto/rand" + "encoding/base64" + "math/big" + "sort" +) + +const ( + alphaUpperAlnum = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + alphaMixedAlnum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + alphaHexLower = "0123456789abcdef" + + keyAWS = "aws" + keyStripe = "stripe" + keyGitHub = "github" + keyDB = "db" +) + +type EnvLine struct { + Comment string + Key string + Value string +} + +type Recipe interface { + Name() string + Generate() []EnvLine +} + +var registry = map[string]Recipe{ + keyAWS: AWS{}, + keyStripe: Stripe{}, + keyGitHub: GitHub{}, + keyDB: DB{}, +} + +func Get(key string) (Recipe, bool) { + r, ok := registry[key] + return r, ok +} + +func AvailableKeys() []string { + keys := make([]string, 0, len(registry)) + for k := range registry { + keys = append(keys, k) + } + sort.Strings(keys) + return keys +} + +func RandomAlnumUpper(length int) string { + return randomString(alphaUpperAlnum, length) +} + +func RandomAlnumMixed(length int) string { + return randomString(alphaMixedAlnum, length) +} + +func RandomHexLower(length int) string { + return randomString(alphaHexLower, length) +} + +func RandomBase64(byteCount int) string { + if byteCount <= 0 { + return "" + } + buf := make([]byte, byteCount) + if _, err := rand.Read(buf); err != nil { + return "" + } + return base64.StdEncoding.EncodeToString(buf) +} + +func RandomChoice(choices []string) string { + if len(choices) == 0 { + return "" + } + idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(choices)))) + if err != nil { + return choices[0] + } + return choices[idx.Int64()] +} + +func randomString(alphabet string, length int) string { + if length <= 0 || alphabet == "" { + return "" + } + out := make([]byte, length) + bigLen := big.NewInt(int64(len(alphabet))) + for i := range out { + idx, err := rand.Int(rand.Reader, bigLen) + if err != nil { + out[i] = alphabet[0] + continue + } + out[i] = alphabet[idx.Int64()] + } + return string(out) +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/recipes_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/recipes_test.go new file mode 100644 index 00000000..7542b978 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/recipes_test.go @@ -0,0 +1,99 @@ +// ©AngelaMos | 2026 +// recipes_test.go + +package recipes_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile/recipes" +) + +func TestGet_KnownKeysReturnRecipes(t *testing.T) { + for _, name := range []string{"aws", "stripe", "github", "db"} { + r, ok := recipes.Get(name) + require.True(t, ok, "Get(%q) must return a recipe", name) + require.NotNil(t, r) + require.Equal(t, name, r.Name()) + } +} + +func TestGet_UnknownKeyReturnsFalse(t *testing.T) { + r, ok := recipes.Get("nonexistent") + require.False(t, ok) + require.Nil(t, r) +} + +func TestAvailableKeys_ReturnsSortedSnapshot(t *testing.T) { + keys := recipes.AvailableKeys() + require.Equal(t, []string{"aws", "db", "github", "stripe"}, keys) +} + +func TestRandomAlnumUpper_LengthAndAlphabet(t *testing.T) { + got := recipes.RandomAlnumUpper(20) + require.Len(t, got, 20) + require.Regexp(t, `^[A-Z0-9]{20}$`, got) +} + +func TestRandomAlnumUpper_ZeroAndNegativeReturnEmpty(t *testing.T) { + require.Empty(t, recipes.RandomAlnumUpper(0)) + require.Empty(t, recipes.RandomAlnumUpper(-1)) +} + +func TestRandomAlnumMixed_LengthAndAlphabet(t *testing.T) { + got := recipes.RandomAlnumMixed(50) + require.Len(t, got, 50) + require.Regexp(t, `^[A-Za-z0-9]{50}$`, got) +} + +func TestRandomHexLower_LengthAndAlphabet(t *testing.T) { + got := recipes.RandomHexLower(32) + require.Len(t, got, 32) + require.Regexp(t, `^[0-9a-f]{32}$`, got) +} + +func TestRandomBase64_DecodesToRequestedBytes(t *testing.T) { + got := recipes.RandomBase64(30) + require.NotEmpty(t, got) + require.Regexp(t, `^[A-Za-z0-9+/]+={0,2}$`, got) +} + +func TestRandomBase64_ZeroReturnsEmpty(t *testing.T) { + require.Empty(t, recipes.RandomBase64(0)) + require.Empty(t, recipes.RandomBase64(-5)) +} + +func TestRandomChoice_ReturnsOneOf(t *testing.T) { + choices := []string{"a", "b", "c", "d", "e"} + for range 50 { + got := recipes.RandomChoice(choices) + require.Contains(t, choices, got) + } +} + +func TestRandomChoice_EmptyReturnsEmpty(t *testing.T) { + require.Empty(t, recipes.RandomChoice(nil)) + require.Empty(t, recipes.RandomChoice([]string{})) +} + +func TestRandomChoice_SingleElementAlwaysReturnsIt(t *testing.T) { + choices := []string{"only"} + for range 20 { + require.Equal(t, "only", recipes.RandomChoice(choices)) + } +} + +func TestRandomness_RepeatedCallsProduceDistinctValues(t *testing.T) { + seen := make(map[string]struct{}) + for range 100 { + seen[recipes.RandomAlnumMixed(20)] = struct{}{} + } + require.Greater( + t, + len(seen), + 95, + "crypto/rand should produce near-100 distinct 20-char alnum strings out of 100", + ) +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/stripe.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/stripe.go new file mode 100644 index 00000000..3255101b --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/stripe.go @@ -0,0 +1,35 @@ +// ©AngelaMos | 2026 +// stripe.go + +package recipes + +const ( + stripeSecretPrefix = "sk_live_" + stripePublishablePrefix = "pk_live_" + stripeWebhookPrefix = "whsec_" + + stripeKeyBodyLen = 24 + stripeWebhookBodyLen = 32 +) + +type Stripe struct{} + +func (Stripe) Name() string { return keyStripe } + +func (Stripe) Generate() []EnvLine { + return []EnvLine{ + {Comment: "Stripe production keys"}, + { + Key: "STRIPE_SECRET_KEY", + Value: stripeSecretPrefix + RandomAlnumMixed(stripeKeyBodyLen), + }, + { + Key: "STRIPE_PUBLISHABLE_KEY", + Value: stripePublishablePrefix + RandomAlnumMixed(stripeKeyBodyLen), + }, + { + Key: "STRIPE_WEBHOOK_SECRET", + Value: stripeWebhookPrefix + RandomAlnumMixed(stripeWebhookBodyLen), + }, + } +} diff --git a/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/stripe_test.go b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/stripe_test.go new file mode 100644 index 00000000..feb939c6 --- /dev/null +++ b/PROJECTS/beginner/canary-token-generator/backend/internal/token/generators/envfile/recipes/stripe_test.go @@ -0,0 +1,78 @@ +// ©AngelaMos | 2026 +// stripe_test.go + +package recipes_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/CarterPerez-dev/cybersecurity-projects/canary-token-generator/backend/internal/token/generators/envfile/recipes" +) + +func TestStripeRecipe_Name(t *testing.T) { + require.Equal(t, "stripe", recipes.Stripe{}.Name()) +} + +func TestStripeRecipe_GeneratesExpectedKeys(t *testing.T) { + lines := recipes.Stripe{}.Generate() + byKey := linesByKey(t, lines) + + for _, key := range []string{ + "STRIPE_SECRET_KEY", + "STRIPE_PUBLISHABLE_KEY", + "STRIPE_WEBHOOK_SECRET", + } { + _, ok := byKey[key] + require.True(t, ok, "Stripe recipe must emit %s", key) + } +} + +func TestStripeRecipe_SecretKeyMatchesLiveFormat(t *testing.T) { + lines := recipes.Stripe{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^sk_live_[A-Za-z0-9]{24}$`, + byKey["STRIPE_SECRET_KEY"].Value, + ) +} + +func TestStripeRecipe_PublishableKeyMatchesLiveFormat(t *testing.T) { + lines := recipes.Stripe{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^pk_live_[A-Za-z0-9]{24}$`, + byKey["STRIPE_PUBLISHABLE_KEY"].Value, + ) +} + +func TestStripeRecipe_WebhookSecretMatchesFormat(t *testing.T) { + lines := recipes.Stripe{}.Generate() + byKey := linesByKey(t, lines) + require.Regexp( + t, + `^whsec_[A-Za-z0-9]{32}$`, + byKey["STRIPE_WEBHOOK_SECRET"].Value, + ) +} + +func TestStripeRecipe_HasLeadingComment(t *testing.T) { + lines := recipes.Stripe{}.Generate() + require.NotEmpty(t, lines) + require.NotEmpty(t, lines[0].Comment) + require.Empty(t, lines[0].Key) +} + +func TestStripeRecipe_KeysAreDistinct(t *testing.T) { + lines := recipes.Stripe{}.Generate() + byKey := linesByKey(t, lines) + require.NotEqual( + t, + byKey["STRIPE_SECRET_KEY"].Value, + byKey["STRIPE_PUBLISHABLE_KEY"].Value, + "secret and publishable keys must have different random bodies", + ) +}