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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ require (
github.com/spf13/cobra v1.10.1
)

require github.com/bmatcuk/doublestar/v4 v4.9.1

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
Expand Down
224 changes: 224 additions & 0 deletions internal/adapter/eslint/adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package eslint

import (
"context"
"os"
"path/filepath"
"testing"

"github.com/DevSymphony/sym-cli/internal/adapter"
"github.com/DevSymphony/sym-cli/internal/engine/core"
)

func TestNewAdapter(t *testing.T) {
adapter := NewAdapter("", "")
if adapter == nil {
t.Fatal("NewAdapter() returned nil")
}

if adapter.ToolsDir == "" {
t.Error("ToolsDir should not be empty")
}
}

func TestNewAdapter_CustomDirs(t *testing.T) {
toolsDir := "/custom/tools"
workDir := "/custom/work"

a := NewAdapter(toolsDir, workDir)

if a.ToolsDir != toolsDir {
t.Errorf("ToolsDir = %q, want %q", a.ToolsDir, toolsDir)
}

if a.WorkDir != workDir {
t.Errorf("WorkDir = %q, want %q", a.WorkDir, workDir)
}
}

func TestName(t *testing.T) {
a := NewAdapter("", "")
if a.Name() != "eslint" {
t.Errorf("Name() = %q, want %q", a.Name(), "eslint")
}
}

func TestGetESLintPath(t *testing.T) {
a := NewAdapter("/test/tools", "")
expected := filepath.Join("/test/tools", "node_modules", ".bin", "eslint")

got := a.getESLintPath()
if got != expected {
t.Errorf("getESLintPath() = %q, want %q", got, expected)
}
}

func TestInitPackageJSON(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "eslint-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)

a := NewAdapter(tmpDir, "")

if err := a.initPackageJSON(); err != nil {
t.Fatalf("initPackageJSON() error = %v", err)
}

packagePath := filepath.Join(tmpDir, "package.json")
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
t.Error("package.json was not created")
}

content, err := os.ReadFile(packagePath)
if err != nil {
t.Fatalf("Failed to read package.json: %v", err)
}

expectedFields := []string{`"name"`, `"symphony-tools"`}
for _, field := range expectedFields {
if !contains(string(content), field) {
t.Errorf("package.json missing expected field: %s", field)
}
}
}

func TestCheckAvailability_NotFound(t *testing.T) {
a := NewAdapter("/nonexistent/path", "")

ctx := context.Background()
err := a.CheckAvailability(ctx)

if err == nil {
t.Log("ESLint found globally, test skipped")
}
}

func TestInstall(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "eslint-install-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)

a := NewAdapter(tmpDir, "")

ctx := context.Background()
config := adapter.InstallConfig{
ToolsDir: tmpDir,
}

err = a.Install(ctx, config)
if err != nil {
t.Logf("Install failed (expected if npm unavailable): %v", err)
}
}

func TestGenerateConfig(t *testing.T) {
a := NewAdapter("", "")

rule := &core.Rule{
ID: "TEST-RULE",
Category: "naming",
Severity: "error",
Check: map[string]interface{}{
"engine": "pattern",
"target": "identifier",
"pattern": "^[A-Z]",
},
}

config, err := a.GenerateConfig(rule)
if err != nil {
t.Fatalf("GenerateConfig() error = %v", err)
}

if len(config) == 0 {
t.Error("GenerateConfig() returned empty config")
}
}

func TestExecute_InvalidConfig(t *testing.T) {
a := NewAdapter("", t.TempDir())

ctx := context.Background()
config := []byte(`{"rules": {}}`)
files := []string{"test.js"}

_, err := a.Execute(ctx, config, files)
if err == nil {
t.Log("Execute succeeded (ESLint may be available)")
}
}

func TestParseOutput(t *testing.T) {
a := NewAdapter("", "")

output := &adapter.ToolOutput{
Stdout: `[{"filePath":"test.js","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'x' is defined but never used","line":1,"column":5}]}]`,
Stderr: "",
ExitCode: 1,
}

violations, err := a.ParseOutput(output)
if err != nil {
t.Fatalf("ParseOutput() error = %v", err)
}

if len(violations) == 0 {
t.Error("Expected violations to be parsed")
}
}

func TestMapSeverity(t *testing.T) {
tests := []struct {
input string
want string
}{
{"error", "error"},
{"warning", "warn"},
{"info", "off"},
{"unknown", "error"}, // default
}

for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := MapSeverity(tt.input)
if got != tt.want {
t.Errorf("MapSeverity(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}

func TestMarshalConfig(t *testing.T) {
config := map[string]interface{}{
"rules": map[string]interface{}{
"semi": []interface{}{2, "always"},
},
}

data, err := MarshalConfig(config)
if err != nil {
t.Fatalf("MarshalConfig() error = %v", err)
}

if len(data) == 0 {
t.Error("MarshalConfig() returned empty data")
}
}

// Helper function
func contains(s, substr string) bool {
return len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)
}

func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
136 changes: 136 additions & 0 deletions internal/adapter/eslint/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,139 @@ func TestGenerateConfig_Style(t *testing.T) {
t.Error("expected semi rule to be set")
}
}

func TestGenerateConfig_PatternContent(t *testing.T) {
rule := &core.Rule{
ID: "TEST-PATTERN-CONTENT",
Category: "security",
Severity: "error",
Check: map[string]interface{}{
"engine": "pattern",
"target": "content",
"pattern": "password",
},
Message: "No hardcoded passwords",
}

config, err := generateConfig(rule)
if err != nil {
t.Fatalf("generateConfig failed: %v", err)
}

var eslintConfig ESLintConfig
if err := json.Unmarshal(config, &eslintConfig); err != nil {
t.Fatalf("failed to parse config: %v", err)
}

if _, ok := eslintConfig.Rules["no-restricted-syntax"]; !ok {
t.Error("expected no-restricted-syntax rule to be set")
}
}

func TestGenerateConfig_PatternImport(t *testing.T) {
rule := &core.Rule{
ID: "TEST-PATTERN-IMPORT",
Category: "dependency",
Severity: "error",
Check: map[string]interface{}{
"engine": "pattern",
"target": "import",
"pattern": "^forbidden-package",
},
}

config, err := generateConfig(rule)
if err != nil {
t.Fatalf("generateConfig failed: %v", err)
}

var eslintConfig ESLintConfig
if err := json.Unmarshal(config, &eslintConfig); err != nil {
t.Fatalf("failed to parse config: %v", err)
}

if _, ok := eslintConfig.Rules["no-restricted-imports"]; !ok {
t.Error("expected no-restricted-imports rule to be set")
}
}

func TestGenerateConfig_LengthFile(t *testing.T) {
rule := &core.Rule{
ID: "TEST-LENGTH-FILE",
Category: "formatting",
Severity: "warning",
Check: map[string]interface{}{
"engine": "length",
"scope": "file",
"max": 500,
},
}

config, err := generateConfig(rule)
if err != nil {
t.Fatalf("generateConfig failed: %v", err)
}

var eslintConfig ESLintConfig
if err := json.Unmarshal(config, &eslintConfig); err != nil {
t.Fatalf("failed to parse config: %v", err)
}

if _, ok := eslintConfig.Rules["max-lines"]; !ok {
t.Error("expected max-lines rule to be set")
}
}

func TestGenerateConfig_LengthFunction(t *testing.T) {
rule := &core.Rule{
ID: "TEST-LENGTH-FUNCTION",
Category: "formatting",
Severity: "warning",
Check: map[string]interface{}{
"engine": "length",
"scope": "function",
"max": 50,
},
}

config, err := generateConfig(rule)
if err != nil {
t.Fatalf("generateConfig failed: %v", err)
}

var eslintConfig ESLintConfig
if err := json.Unmarshal(config, &eslintConfig); err != nil {
t.Fatalf("failed to parse config: %v", err)
}

if _, ok := eslintConfig.Rules["max-lines-per-function"]; !ok {
t.Error("expected max-lines-per-function rule to be set")
}
}

func TestGenerateConfig_LengthParams(t *testing.T) {
rule := &core.Rule{
ID: "TEST-LENGTH-PARAMS",
Category: "formatting",
Severity: "warning",
Check: map[string]interface{}{
"engine": "length",
"scope": "params",
"max": 4,
},
}

config, err := generateConfig(rule)
if err != nil {
t.Fatalf("generateConfig failed: %v", err)
}

var eslintConfig ESLintConfig
if err := json.Unmarshal(config, &eslintConfig); err != nil {
t.Fatalf("failed to parse config: %v", err)
}

if _, ok := eslintConfig.Rules["max-params"]; !ok {
t.Error("expected max-params rule to be set")
}
}
Loading