Skip to content

Commit d254522

Browse files
authored
Fix/allowance of space in config name creates split config (#4)
* forbid special chars in alias * add new validation package & add validation for config names
1 parent 8cb7368 commit d254522

6 files changed

Lines changed: 298 additions & 7 deletions

File tree

pkg/tui/commands/config.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"tusshi/pkg/config"
10+
"tusshi/pkg/validation"
1011
)
1112

1213
const tabAll = "All"
@@ -27,6 +28,11 @@ func AddConfig(mgr *config.Manager, parts []string) func(Context) {
2728
targetPath = filepath.Join(filepath.Dir(mgr.PrimaryPath), arg)
2829
}
2930

31+
if err := validation.ValidateConfigName(filepath.Base(targetPath)); err != nil {
32+
ctx.SetError("Invalid config name: " + err.Error())
33+
return
34+
}
35+
3036
if err := mgr.AddConfigFile(targetPath); err != nil {
3137
ctx.SetError("Add config error: " + err.Error())
3238
} else {
@@ -83,6 +89,11 @@ func RenameConfig(mgr *config.Manager, parts []string) func(Context) {
8389
newPath = filepath.Join(filepath.Dir(mgr.PrimaryPath), newName)
8490
}
8591

92+
if err := validation.ValidateConfigName(filepath.Base(newPath)); err != nil {
93+
ctx.SetError("Invalid config name: " + err.Error())
94+
return
95+
}
96+
8697
if err := mgr.RenameConfigFile(oldPath, newPath); err != nil {
8798
ctx.SetError("Rename config error: " + err.Error())
8899
} else {

pkg/tui/forms.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package tui
22

33
import (
4-
"errors"
54
"path/filepath"
65

76
"tusshi/pkg/config"
7+
"tusshi/pkg/validation"
88

99
"github.com/charmbracelet/huh"
1010
)
@@ -65,12 +65,7 @@ func (m *Model) BuildHostForm(defaultFile string) *huh.Form {
6565
Description("What you will type to connect (e.g. prod-web-01)").
6666
Placeholder("my-server").
6767
Value(&m.FormHost.Alias).
68-
Validate(func(str string) error {
69-
if str == "" {
70-
return errors.New("alias is required")
71-
}
72-
return nil
73-
}),
68+
Validate(validation.ValidateAlias),
7469
huh.NewInput().
7570
Title("Server Address / HostName").
7671
Description("Domain or IP address of the target server").

pkg/tui/tui_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,25 @@ func TestTUIConfigCommands(t *testing.T) {
110110
subPath := filepath.Join(tmpDir, "sub-config-tui")
111111
renamedPath := filepath.Join(tmpDir, "renamed-config-tui")
112112

113+
t.Run("add-config validation error", func(t *testing.T) {
114+
m.ErrorText = ""
115+
_, _ = m.executeCommand("add-config invalid*config")
116+
assert.Contains(t, m.ErrorText, "Invalid config name")
117+
})
118+
113119
t.Run("add-config", func(t *testing.T) {
114120
_, _ = m.executeCommand("add-config " + subPath)
115121
assert.FileExists(t, subPath)
116122
assert.Contains(t, m.Manager.FileOrder, subPath)
117123
assert.Equal(t, subPath, m.ActiveTab)
118124
})
119125

126+
t.Run("rename-config validation error", func(t *testing.T) {
127+
m.ErrorText = ""
128+
_, _ = m.executeCommand("rename-config invalid*rename")
129+
assert.Contains(t, m.ErrorText, "Invalid config name")
130+
})
131+
120132
t.Run("rename-config", func(t *testing.T) {
121133
_, _ = m.executeCommand("rename-config " + renamedPath)
122134
assert.FileExists(t, renamedPath)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Package validation contains testing suites for input validation functions.
2+
package validation
3+
4+
import (
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
// TestValidateAlias checks the SSH host connection alias validation rules.
11+
func TestValidateAlias(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
alias string
15+
wantErr bool
16+
errMsg string
17+
}{
18+
{
19+
name: "empty alias",
20+
alias: "",
21+
wantErr: true,
22+
errMsg: "alias is required",
23+
},
24+
{
25+
name: "alias with spaces",
26+
alias: "my server",
27+
wantErr: true,
28+
errMsg: "alias cannot contain spaces",
29+
},
30+
{
31+
name: "alias with multiple spaces",
32+
alias: "my server",
33+
wantErr: true,
34+
errMsg: "alias cannot contain spaces",
35+
},
36+
{
37+
name: "valid alias",
38+
alias: "my-server-01",
39+
wantErr: false,
40+
},
41+
{
42+
name: "forbidden less than",
43+
alias: "server<01",
44+
wantErr: true,
45+
errMsg: "alias cannot contain forbidden characters",
46+
},
47+
{
48+
name: "forbidden greater than",
49+
alias: "server>01",
50+
wantErr: true,
51+
errMsg: "alias cannot contain forbidden characters",
52+
},
53+
{
54+
name: "forbidden colon",
55+
alias: "server:01",
56+
wantErr: true,
57+
errMsg: "alias cannot contain forbidden characters",
58+
},
59+
{
60+
name: "forbidden double quote",
61+
alias: `server"01`,
62+
wantErr: true,
63+
errMsg: "alias cannot contain forbidden characters",
64+
},
65+
{
66+
name: "forbidden forward slash",
67+
alias: "server/01",
68+
wantErr: true,
69+
errMsg: "alias cannot contain forbidden characters",
70+
},
71+
{
72+
name: "forbidden backslash",
73+
alias: `server\01`,
74+
wantErr: true,
75+
errMsg: "alias cannot contain forbidden characters",
76+
},
77+
{
78+
name: "forbidden pipe",
79+
alias: "server|01",
80+
wantErr: true,
81+
errMsg: "alias cannot contain forbidden characters",
82+
},
83+
{
84+
name: "forbidden question mark",
85+
alias: "server?01",
86+
wantErr: true,
87+
errMsg: "alias cannot contain forbidden characters",
88+
},
89+
{
90+
name: "forbidden asterisk",
91+
alias: "server*01",
92+
wantErr: true,
93+
errMsg: "alias cannot contain forbidden characters",
94+
},
95+
{
96+
name: "forbidden dollar",
97+
alias: "server$01",
98+
wantErr: true,
99+
errMsg: "alias cannot contain forbidden characters",
100+
},
101+
}
102+
103+
for _, tt := range tests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
err := ValidateAlias(tt.alias)
106+
if tt.wantErr {
107+
assert.Error(t, err)
108+
assert.Contains(t, err.Error(), tt.errMsg)
109+
} else {
110+
assert.NoError(t, err)
111+
}
112+
})
113+
}
114+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Package validation contains testing suites for input validation functions.
2+
package validation
3+
4+
import (
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
// TestValidateConfigName checks the SSH config file name validation rules.
11+
func TestValidateConfigName(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
config string
15+
wantErr bool
16+
errMsg string
17+
}{
18+
{
19+
name: "empty config name",
20+
config: "",
21+
wantErr: true,
22+
errMsg: "config name is required",
23+
},
24+
{
25+
name: "config name with spaces",
26+
config: "my config",
27+
wantErr: true,
28+
errMsg: "config name cannot contain spaces",
29+
},
30+
{
31+
name: "config name with multiple spaces",
32+
config: "my config",
33+
wantErr: true,
34+
errMsg: "config name cannot contain spaces",
35+
},
36+
{
37+
name: "valid config name",
38+
config: "my-config",
39+
wantErr: false,
40+
},
41+
{
42+
name: "valid config name with extension",
43+
config: "config.txt",
44+
wantErr: false,
45+
},
46+
{
47+
name: "forbidden less than",
48+
config: "config<01",
49+
wantErr: true,
50+
errMsg: "config name cannot contain forbidden characters",
51+
},
52+
{
53+
name: "forbidden greater than",
54+
config: "config>01",
55+
wantErr: true,
56+
errMsg: "config name cannot contain forbidden characters",
57+
},
58+
{
59+
name: "forbidden colon",
60+
config: "config:01",
61+
wantErr: true,
62+
errMsg: "config name cannot contain forbidden characters",
63+
},
64+
{
65+
name: "forbidden double quote",
66+
config: `config"01`,
67+
wantErr: true,
68+
errMsg: "config name cannot contain forbidden characters",
69+
},
70+
{
71+
name: "forbidden forward slash",
72+
config: "config/01",
73+
wantErr: true,
74+
errMsg: "config name cannot contain forbidden characters",
75+
},
76+
{
77+
name: "forbidden backslash",
78+
config: `config\01`,
79+
wantErr: true,
80+
errMsg: "config name cannot contain forbidden characters",
81+
},
82+
{
83+
name: "forbidden pipe",
84+
config: "config|01",
85+
wantErr: true,
86+
errMsg: "config name cannot contain forbidden characters",
87+
},
88+
{
89+
name: "forbidden question mark",
90+
config: "config?01",
91+
wantErr: true,
92+
errMsg: "config name cannot contain forbidden characters",
93+
},
94+
{
95+
name: "forbidden asterisk",
96+
config: "config*01",
97+
wantErr: true,
98+
errMsg: "config name cannot contain forbidden characters",
99+
},
100+
{
101+
name: "forbidden hash",
102+
config: "config#01",
103+
wantErr: true,
104+
errMsg: "config name cannot contain forbidden characters",
105+
},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
err := ValidateConfigName(tt.config)
111+
if tt.wantErr {
112+
assert.Error(t, err)
113+
assert.Contains(t, err.Error(), tt.errMsg)
114+
} else {
115+
assert.NoError(t, err)
116+
}
117+
})
118+
}
119+
}

pkg/validation/validation.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Package validation provides validation logic for connection aliases and config names.
2+
package validation
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"regexp"
8+
"strings"
9+
)
10+
11+
var forbiddenAliasChars = regexp.MustCompile(`[<>:"/\\|?*$]`)
12+
var forbiddenConfigNameChars = regexp.MustCompile(`[<>:"/\\|?*#]`)
13+
14+
// ValidateAlias checks if the alias matches the validation rules.
15+
func ValidateAlias(str string) error {
16+
if str == "" {
17+
return errors.New("alias is required")
18+
}
19+
if len(strings.Split(str, " ")) > 1 {
20+
return errors.New("alias cannot contain spaces")
21+
}
22+
if forbiddenAliasChars.MatchString(str) {
23+
return fmt.Errorf("alias cannot contain forbidden characters (%v)", strings.Split(forbiddenAliasChars.String(), ""))
24+
}
25+
return nil
26+
}
27+
28+
// ValidateConfigName checks if the configuration name matches the validation rules.
29+
func ValidateConfigName(str string) error {
30+
if str == "" {
31+
return errors.New("config name is required")
32+
}
33+
if len(strings.Split(str, " ")) > 1 {
34+
return errors.New("config name cannot contain spaces")
35+
}
36+
if forbiddenConfigNameChars.MatchString(str) {
37+
return fmt.Errorf("config name cannot contain forbidden characters (%v)", strings.Split(forbiddenConfigNameChars.String(), ""))
38+
}
39+
return nil
40+
}

0 commit comments

Comments
 (0)