diff --git a/internal/check/env.go b/internal/check/env.go new file mode 100644 index 0000000..932eb9b --- /dev/null +++ b/internal/check/env.go @@ -0,0 +1,83 @@ +package check + +import ( + "bufio" + "context" + "fmt" + "os" + "strings" +) + +type EnvCheck struct { + Dir string +} + +func (c *EnvCheck) Name() string { + return ".env has all required keys" +} + +func (c *EnvCheck) Run(_ context.Context) Result { + exampleKeys, err := parseEnvKeys(c.Dir + "/.env.example") + if err != nil { + return Result{ + Name: c.Name(), + Status: StatusFail, + Message: "could not read .env.example", + } + } + + actualKeys, err := parseEnvKeys(c.Dir + "/.env") + if err != nil { + return Result{ + Name: c.Name(), + Status: StatusFail, + Message: ".env file not found", + Fix: "copy .env.example to .env and fill in the values", + } + } + + var missing []string + for key := range exampleKeys { + if _, ok := actualKeys[key]; !ok { + missing = append(missing, key) + } + } + + if len(missing) > 0 { + return Result{ + Name: c.Name(), + Status: StatusFail, + Message: fmt.Sprintf("missing keys: %s", strings.Join(missing, ", ")), + Fix: "add the missing keys to your .env file", + } + } + + return Result{ + Name: c.Name(), + Status: StatusPass, + Message: fmt.Sprintf("all %d keys present", len(exampleKeys)), + } +} + +func parseEnvKeys(path string) (map[string]struct{}, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + keys := make(map[string]struct{}) + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, _, _ := strings.Cut(line, "=") + key = strings.TrimSpace(key) + if key != "" { + keys[key] = struct{}{} + } + } + return keys, scanner.Err() +} diff --git a/internal/check/env_test.go b/internal/check/env_test.go new file mode 100644 index 0000000..ef3d2d0 --- /dev/null +++ b/internal/check/env_test.go @@ -0,0 +1,54 @@ +package check + +import ( + "context" + "os" + "testing" +) + +func TestEnvCheck_Pass(t *testing.T) { + dir := t.TempDir() + os.WriteFile(dir+"/.env.example", []byte("DB_URL=\nAPI_KEY=\n"), 0644) + os.WriteFile(dir+"/.env", []byte("DB_URL=postgres://localhost\nAPI_KEY=secret\n"), 0644) + + c := &EnvCheck{Dir: dir} + result := c.Run(context.Background()) + if result.Status != StatusPass { + t.Errorf("expected pass, got %v: %s", result.Status, result.Message) + } +} + +func TestEnvCheck_MissingKeys(t *testing.T) { + dir := t.TempDir() + os.WriteFile(dir+"/.env.example", []byte("DB_URL=\nAPI_KEY=\nSECRET=\n"), 0644) + os.WriteFile(dir+"/.env", []byte("DB_URL=postgres://localhost\n"), 0644) + + c := &EnvCheck{Dir: dir} + result := c.Run(context.Background()) + if result.Status != StatusFail { + t.Errorf("expected fail, got %v", result.Status) + } +} + +func TestEnvCheck_MissingEnvFile(t *testing.T) { + dir := t.TempDir() + os.WriteFile(dir+"/.env.example", []byte("DB_URL=\n"), 0644) + + c := &EnvCheck{Dir: dir} + result := c.Run(context.Background()) + if result.Status != StatusFail { + t.Errorf("expected fail, got %v", result.Status) + } +} + +func TestEnvCheck_IgnoresComments(t *testing.T) { + dir := t.TempDir() + os.WriteFile(dir+"/.env.example", []byte("# this is a comment\nDB_URL=\n\nAPI_KEY=\n"), 0644) + os.WriteFile(dir+"/.env", []byte("DB_URL=postgres://localhost\nAPI_KEY=secret\n"), 0644) + + c := &EnvCheck{Dir: dir} + result := c.Run(context.Background()) + if result.Status != StatusPass { + t.Errorf("expected pass, got %v: %s", result.Status, result.Message) + } +} diff --git a/internal/check/registry.go b/internal/check/registry.go index 12e827e..9faa351 100644 --- a/internal/check/registry.go +++ b/internal/check/registry.go @@ -35,8 +35,9 @@ func Build(stack detector.DetectedStack) []Check { // add Redis checks } - // always run env check if .env.example exists - // cs = append(cs, &EnvCheck{}) + if stack.EnvExample { + cs = append(cs, &EnvCheck{Dir: "."}) + } return cs } diff --git a/internal/detector/detector.go b/internal/detector/detector.go index e579a15..3aa2812 100644 --- a/internal/detector/detector.go +++ b/internal/detector/detector.go @@ -17,7 +17,8 @@ type DetectedStack struct { Postgres bool Redis bool MySQL bool - MongoDB bool + MongoDB bool + EnvExample bool } func Detect(dir string) DetectedStack { @@ -39,6 +40,7 @@ func Detect(dir string) DetectedStack { stack.MySQL = strings.Contains(dbURL, "mysql") stack.MongoDB = os.Getenv("MONGODB_URI") != "" || os.Getenv("MONGO_URL") != "" stack.Redis = os.Getenv("REDIS_URL") != "" || os.Getenv("REDIS_URI") != "" + stack.EnvExample = fileExists(filepath.Join(dir, ".env.example")) return stack }