blob: 0c86550d114ceebad6b99b6c2eea869cccb5005a [file] [log] [blame]
package bashkit
import (
"strings"
"testing"
"mvdan.cc/sh/v3/syntax"
)
func TestCheck(t *testing.T) {
tests := []struct {
name string
script string
wantErr bool
errMatch string // string to match in error message, if wantErr is true
}{
{
name: "valid script",
script: "echo hello world",
wantErr: false,
errMatch: "",
},
{
name: "invalid syntax",
script: "echo 'unterminated string",
wantErr: false, // As per implementation, syntax errors are not flagged
errMatch: "",
},
{
name: "git config user.name",
script: "git config user.name 'John Doe'",
wantErr: true,
errMatch: "changing git config username/email is not allowed",
},
{
name: "git config user.email",
script: "git config user.email 'john@example.com'",
wantErr: true,
errMatch: "changing git config username/email is not allowed",
},
{
name: "git config with flag user.name",
script: "git config --global user.name 'John Doe'",
wantErr: true,
errMatch: "changing git config username/email is not allowed",
},
{
name: "git config with other setting",
script: "git config core.editor vim",
wantErr: false,
errMatch: "",
},
{
name: "git without config",
script: "git commit -m 'Add feature'",
wantErr: false,
errMatch: "",
},
{
name: "multiline script with proper escaped newlines",
script: "echo 'Setting up git...' && git config user.name 'John Doe' && echo 'Done!'",
wantErr: true,
errMatch: "changing git config username/email is not allowed",
},
{
name: "multiline script with backticks",
script: `echo 'Setting up git...'
git config user.name 'John Doe'
echo 'Done!'`,
wantErr: true,
errMatch: "changing git config username/email is not allowed",
},
{
name: "git config with variable",
script: "NAME='John Doe'\ngit config user.name $NAME",
wantErr: true,
errMatch: "changing git config username/email is not allowed",
},
{
name: "only git command",
script: "git",
wantErr: false,
errMatch: "",
},
{
name: "read git config",
script: "git config user.name",
wantErr: false,
errMatch: "",
},
{
name: "commented git config",
script: "# git config user.name 'John Doe'",
wantErr: false,
errMatch: "",
},
// Git add validation tests
{
name: "git add with -A flag",
script: "git add -A",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with --all flag",
script: "git add --all",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with dot",
script: "git add .",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with asterisk",
script: "git add *",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with multiple flags including -A",
script: "git add -v -A",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with specific file",
script: "git add main.go",
wantErr: false,
errMatch: "",
},
{
name: "git add with multiple specific files",
script: "git add main.go utils.go",
wantErr: false,
errMatch: "",
},
{
name: "git add with directory path",
script: "git add src/main.go",
wantErr: false,
errMatch: "",
},
{
name: "git add with git flags before add",
script: "git -C /path/to/repo add -A",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with valid flags",
script: "git add -v main.go",
wantErr: false,
errMatch: "",
},
{
name: "git command without add",
script: "git status",
wantErr: false,
errMatch: "",
},
{
name: "multiline script with blind git add",
script: "echo 'Adding files' && git add -A && git commit -m 'Update'",
wantErr: true,
errMatch: "blind git add commands",
},
{
name: "git add with pattern that looks like blind but is specific",
script: "git add file.A",
wantErr: false,
errMatch: "",
},
{
name: "commented blind git add",
script: "# git add -A",
wantErr: false,
errMatch: "",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := Check(tc.script)
if (err != nil) != tc.wantErr {
t.Errorf("Check() error = %v, wantErr %v", err, tc.wantErr)
return
}
if tc.wantErr && err != nil && !strings.Contains(err.Error(), tc.errMatch) {
t.Errorf("Check() error message = %v, want containing %v", err, tc.errMatch)
}
})
}
}
func TestWillRunGitCommit(t *testing.T) {
tests := []struct {
name string
script string
wantCommit bool
}{
{
name: "simple git commit",
script: "git commit -m 'Add feature'",
wantCommit: true,
},
{
name: "git command without commit",
script: "git status",
wantCommit: false,
},
{
name: "multiline script with git commit",
script: "echo 'Making changes' && git add . && git commit -m 'Update files'",
wantCommit: true,
},
{
name: "multiline script without git commit",
script: "echo 'Checking status' && git status",
wantCommit: false,
},
{
name: "script with commented git commit",
script: "# git commit -m 'This is commented out'",
wantCommit: false,
},
{
name: "git commit with variables",
script: "MSG='Fix bug' && git commit -m 'Using variable'",
wantCommit: true,
},
{
name: "only git command",
script: "git",
wantCommit: false,
},
{
name: "script with invalid syntax",
script: "git commit -m 'unterminated string",
wantCommit: false,
},
{
name: "commit used in different context",
script: "echo 'commit message'",
wantCommit: false,
},
{
name: "git with flags before commit",
script: "git -C /path/to/repo commit -m 'Update'",
wantCommit: true,
},
{
name: "git with multiple flags",
script: "git --git-dir=.git -C repo commit -a -m 'Update'",
wantCommit: true,
},
{
name: "git with env vars",
script: "GIT_AUTHOR_NAME=\"Josh Bleecher Snyder\" GIT_AUTHOR_EMAIL=\"josharian@gmail.com\" git commit -am \"Updated code\"",
wantCommit: true,
},
{
name: "git with redirections",
script: "git commit -m 'Fix issue' > output.log 2>&1",
wantCommit: true,
},
{
name: "git with piped commands",
script: "echo 'Committing' | git commit -F -",
wantCommit: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
gotCommit, err := WillRunGitCommit(tc.script)
if err != nil {
t.Errorf("WillRunGitCommit() error = %v", err)
return
}
if gotCommit != tc.wantCommit {
t.Errorf("WillRunGitCommit() = %v, want %v", gotCommit, tc.wantCommit)
}
})
}
}
func TestSketchWipBranchProtection(t *testing.T) {
tests := []struct {
name string
script string
wantErr bool
errMatch string
resetBefore bool // if true, reset warning state before test
}{
{
name: "git branch rename sketch-wip",
script: "git branch -m sketch-wip new-branch",
wantErr: true,
errMatch: "cannot leave 'sketch-wip' branch",
resetBefore: true,
},
{
name: "git branch force rename sketch-wip",
script: "git branch -M sketch-wip new-branch",
wantErr: false, // second call should not error (already warned)
errMatch: "",
resetBefore: false,
},
{
name: "git checkout to other branch",
script: "git checkout main",
wantErr: false, // third call should not error (already warned)
errMatch: "",
resetBefore: false,
},
{
name: "git switch to other branch",
script: "git switch main",
wantErr: false, // fourth call should not error (already warned)
errMatch: "",
resetBefore: false,
},
{
name: "git checkout file (should be allowed)",
script: "git checkout -- file.txt",
wantErr: false,
errMatch: "",
resetBefore: false,
},
{
name: "git checkout path (should be allowed)",
script: "git checkout -- src/main.go",
wantErr: false,
errMatch: "",
resetBefore: false,
},
{
name: "git commit (should be allowed)",
script: "git commit -m 'test'",
wantErr: false,
errMatch: "",
resetBefore: false,
},
{
name: "git status (should be allowed)",
script: "git status",
wantErr: false,
errMatch: "",
resetBefore: false,
},
{
name: "git branch rename other branch (should be allowed)",
script: "git branch -m old-branch new-branch",
wantErr: false,
errMatch: "",
resetBefore: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.resetBefore {
ResetSketchWipWarning()
}
err := Check(tc.script)
if (err != nil) != tc.wantErr {
t.Errorf("Check() error = %v, wantErr %v", err, tc.wantErr)
return
}
if tc.wantErr && err != nil && !strings.Contains(err.Error(), tc.errMatch) {
t.Errorf("Check() error message = %v, want containing %v", err, tc.errMatch)
}
})
}
}
func TestHasSketchWipBranchChanges(t *testing.T) {
tests := []struct {
name string
script string
wantHas bool
}{
{
name: "git branch rename sketch-wip",
script: "git branch -m sketch-wip new-branch",
wantHas: true,
},
{
name: "git branch force rename sketch-wip",
script: "git branch -M sketch-wip new-branch",
wantHas: true,
},
{
name: "git checkout to branch",
script: "git checkout main",
wantHas: true,
},
{
name: "git switch to branch",
script: "git switch main",
wantHas: true,
},
{
name: "git checkout file",
script: "git checkout -- file.txt",
wantHas: false,
},
{
name: "git checkout path",
script: "git checkout src/main.go",
wantHas: false,
},
{
name: "git checkout with .extension",
script: "git checkout file.go",
wantHas: false,
},
{
name: "git status",
script: "git status",
wantHas: false,
},
{
name: "git commit",
script: "git commit -m 'test'",
wantHas: false,
},
{
name: "git branch rename other",
script: "git branch -m old-branch new-branch",
wantHas: false,
},
{
name: "git switch with flag",
script: "git switch -c new-branch",
wantHas: false,
},
{
name: "git checkout with flag",
script: "git checkout -b new-branch",
wantHas: false,
},
{
name: "not a git command",
script: "echo hello",
wantHas: false,
},
{
name: "empty command",
script: "",
wantHas: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
r := strings.NewReader(tc.script)
parser := syntax.NewParser()
file, err := parser.Parse(r, "")
if err != nil {
if tc.wantHas {
t.Errorf("Parse error: %v", err)
}
return
}
found := false
syntax.Walk(file, func(node syntax.Node) bool {
callExpr, ok := node.(*syntax.CallExpr)
if !ok {
return true
}
if hasSketchWipBranchChanges(callExpr) {
found = true
return false
}
return true
})
if found != tc.wantHas {
t.Errorf("hasSketchWipBranchChanges() = %v, want %v", found, tc.wantHas)
}
})
}
}
func TestEdgeCases(t *testing.T) {
tests := []struct {
name string
script string
wantErr bool
resetBefore bool // if true, reset warning state before test
}{
{
name: "git branch -m with current branch to sketch-wip (should be allowed)",
script: "git branch -m current-branch sketch-wip",
wantErr: false,
resetBefore: true,
},
{
name: "git branch -m sketch-wip with no destination (should be blocked)",
script: "git branch -m sketch-wip",
wantErr: true,
resetBefore: true,
},
{
name: "git branch -M with current branch to sketch-wip (should be allowed)",
script: "git branch -M current-branch sketch-wip",
wantErr: false,
resetBefore: true,
},
{
name: "git checkout with -- flags (should be allowed)",
script: "git checkout -- --weird-filename",
wantErr: false,
resetBefore: true,
},
{
name: "git switch with create flag (should be allowed)",
script: "git switch --create new-branch",
wantErr: false,
resetBefore: true,
},
{
name: "complex git command with sketch-wip rename",
script: "git add . && git commit -m \"test\" && git branch -m sketch-wip production",
wantErr: true,
resetBefore: true,
},
{
name: "git switch with -c short form (should be allowed)",
script: "git switch -c feature-branch",
wantErr: false,
resetBefore: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.resetBefore {
ResetSketchWipWarning()
}
err := Check(tc.script)
if (err != nil) != tc.wantErr {
t.Errorf("Check() error = %v, wantErr %v", err, tc.wantErr)
}
})
}
}