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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ Run a server that exports model and lints whenever the input MPR file changes. T

```
./bin/mxlint-cli-darwin-arm64 --config mxlint.yaml serve
INFO[0000] Rules directory .mendix-cache/rules found
INFO[0000] Syncing 2 rulesets to .mendix-cache/rules
INFO[0000] Starting server on port 8084
INFO[0000] Watching for changes in /Users/xcheng/project
INFO[0000] Output directory: modelsource
Expand All @@ -323,14 +325,14 @@ The serve command provides:

## test-rules

Rules can be written in both `Rego` and `JavaScript` format. To speed up rule development we have implemented `test-rules` subcommand that can quickly evaluate your rule against known test scenarios. The test cases are written in `yaml` format.
Rules can be written in both `Rego` and `JavaScript` format. To speed up rule development we have implemented `test-rules` subcommand that can quickly evaluate your rule against known test scenarios. The test cases are written in `yaml` format.

```
$ ./bin/mxlint-darwin-arm64 --config .ci/test-rules.yaml test-rules
INFO[0000] >> resources/rules/001_0002_demo_users_disabled.js
INFO[0000] >> resources/rules/001_0002_demo_users_disabled.js
INFO[0000] PASS allow
INFO[0000] PASS no_allow
INFO[0000] >> resources/rules/001_0003_security_checks.rego
INFO[0000] >> resources/rules/001_0003_security_checks.rego
INFO[0000] PASS allow
INFO[0000] PASS no_allow_1
INFO[0000] PASS no_allow_2
Expand Down
8 changes: 8 additions & 0 deletions serve/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ func runServe(cmd *cobra.Command, args []string) {
log.Infof("Rules directory %s found", rulesDirectory)
}

// Sync rulesets from config if specified
if config != nil && len(config.Rules.Rulesets) > 0 {
log.Infof("Syncing %d rulesets to %s", len(config.Rules.Rulesets), rulesDirectory)
if err := lint.SyncRulesets(config.Rules.Rulesets, rulesDirectory, projectDir); err != nil {
log.Fatalf("Failed to sync rulesets: %v", err)
}
}

expandedPath, err := filepath.Abs(inputDirectory)
if err != nil {
log.Fatalln(err)
Expand Down
161 changes: 161 additions & 0 deletions serve/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/fsnotify/fsnotify"
"github.com/mxlint/mxlint-cli/lint"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -226,3 +227,163 @@ func TestAddDirsRecursive(t *testing.T) {
assert.NotContains(t, path, ".hidden", "Hidden directories should not be watched")
}
}

func TestRulesetSync(t *testing.T) {
// This test verifies that the serve command syncs rulesets from config before starting
// Create a temporary directory structure
tempDir, err := os.MkdirTemp("", "mxlint-ruleset-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

// Create source rules directory with a test rule
sourceRulesDir := filepath.Join(tempDir, "source-rules")
err = os.MkdirAll(filepath.Join(sourceRulesDir, "test-category"), 0755)
assert.NoError(t, err)

testRuleContent := `# METADATA
# title: Test Rule
# description: A test rule for syncing
# custom:
# rulenumber: "001_0001"
# category: "Test"
package test.rules

rule_test {
true
}`
testRulePath := filepath.Join(sourceRulesDir, "test-category", "test_rule.rego")
err = os.WriteFile(testRulePath, []byte(testRuleContent), 0644)
assert.NoError(t, err)

// Create target rules directory (where rules will be synced to)
targetRulesDir := filepath.Join(tempDir, "target-rules")
err = os.MkdirAll(targetRulesDir, 0755)
assert.NoError(t, err)

// Create a config with rulesets
config := &lint.Config{
Rules: lint.ConfigRulesSpec{
Path: targetRulesDir,
Rulesets: []string{
"file://" + filepath.Base(sourceRulesDir),
},
},
}

// Verify source rule exists
assert.FileExists(t, testRulePath, "Source rule should exist")

// Verify target doesn't have the rule yet
targetRulePath := filepath.Join(targetRulesDir, "test-category", "test_rule.rego")
_, err = os.Stat(targetRulePath)
assert.True(t, os.IsNotExist(err), "Target rule should not exist before sync")

// Perform the sync (this is what serve.go does at lines 89-95)
if config != nil && len(config.Rules.Rulesets) > 0 {
log := logrus.New()
log.SetOutput(io.Discard)
lint.SetLogger(log)

err = lint.SyncRulesets(config.Rules.Rulesets, targetRulesDir, tempDir)
assert.NoError(t, err, "SyncRulesets should succeed")
}

// Verify the rule was synced to target
assert.FileExists(t, targetRulePath, "Target rule should exist after sync")

// Verify the content matches
targetContent, err := os.ReadFile(targetRulePath)
assert.NoError(t, err)
assert.Equal(t, testRuleContent, string(targetContent), "Synced rule content should match source")
}

func TestRulesetSyncWithEmptyRulesets(t *testing.T) {
// This test verifies that when config.Rules.Rulesets is empty, no sync occurs
tempDir, err := os.MkdirTemp("", "mxlint-no-ruleset-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

targetRulesDir := filepath.Join(tempDir, "target-rules")
err = os.MkdirAll(targetRulesDir, 0755)
assert.NoError(t, err)

// Create a config with NO rulesets
config := &lint.Config{
Rules: lint.ConfigRulesSpec{
Path: targetRulesDir,
Rulesets: []string{}, // Empty rulesets
},
}

// The sync logic should not run when rulesets is empty (as per serve.go lines 90)
if config != nil && len(config.Rules.Rulesets) > 0 {
t.Fatal("Should not reach here - rulesets is empty")
}

// Verify the target directory is still empty (no sync happened)
entries, err := os.ReadDir(targetRulesDir)
assert.NoError(t, err)
assert.Empty(t, entries, "Target rules directory should remain empty when no rulesets configured")
}

func TestRulesetSyncWithMultipleRulesets(t *testing.T) {
// This test verifies that multiple rulesets can be synced
tempDir, err := os.MkdirTemp("", "mxlint-multi-ruleset-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

// Create two source rules directories
sourceRules1 := filepath.Join(tempDir, "source-rules-1")
sourceRules2 := filepath.Join(tempDir, "source-rules-2")

err = os.MkdirAll(filepath.Join(sourceRules1, "category1"), 0755)
assert.NoError(t, err)
err = os.MkdirAll(filepath.Join(sourceRules2, "category2"), 0755)
assert.NoError(t, err)

// Create test rules in each source
rule1Content := `# METADATA
# title: Rule 1
package test.rule1`
rule1Path := filepath.Join(sourceRules1, "category1", "rule1.rego")
err = os.WriteFile(rule1Path, []byte(rule1Content), 0644)
assert.NoError(t, err)

rule2Content := `# METADATA
# title: Rule 2
package test.rule2`
rule2Path := filepath.Join(sourceRules2, "category2", "rule2.rego")
err = os.WriteFile(rule2Path, []byte(rule2Content), 0644)
assert.NoError(t, err)

// Create target rules directory
targetRulesDir := filepath.Join(tempDir, "target-rules")
err = os.MkdirAll(targetRulesDir, 0755)
assert.NoError(t, err)

// Create config with multiple rulesets
config := &lint.Config{
Rules: lint.ConfigRulesSpec{
Path: targetRulesDir,
Rulesets: []string{
"file://" + filepath.Base(sourceRules1),
"file://" + filepath.Base(sourceRules2),
},
},
}

// Perform the sync
log := logrus.New()
log.SetOutput(io.Discard)
lint.SetLogger(log)

err = lint.SyncRulesets(config.Rules.Rulesets, targetRulesDir, tempDir)
assert.NoError(t, err, "SyncRulesets should succeed with multiple rulesets")

// Verify both rules were synced
targetRule1 := filepath.Join(targetRulesDir, "category1", "rule1.rego")
targetRule2 := filepath.Join(targetRulesDir, "category2", "rule2.rego")

assert.FileExists(t, targetRule1, "Rule from first ruleset should be synced")
assert.FileExists(t, targetRule2, "Rule from second ruleset should be synced")
}
Loading