Skip to content

Commit d5496af

Browse files
authored
fix: correct URL validation and centralize logic (#421)
* fix: correct URL validation and centralize logic * fix: go imports order * tests: add validation URL tests * revert: parameter_test ValidDefaultWithOptions test update * test: update validation_test to use require * test: update validation_test tests
1 parent f239e51 commit d5496af

File tree

8 files changed

+279
-48
lines changed

8 files changed

+279
-48
lines changed

provider/app.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ package provider
22

33
import (
44
"context"
5-
"net/url"
65
"regexp"
76

87
"github.com/google/uuid"
98
"github.com/hashicorp/go-cty/cty"
109
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1110
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
12+
"github.com/coder/terraform-provider-coder/v2/provider/helpers"
1213
)
1314

1415
var (
@@ -93,15 +94,9 @@ func appResource() *schema.Resource {
9394
Description: "A URL to an icon that will display in the dashboard. View built-in " +
9495
"icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " +
9596
"built-in icon with `\"${data.coder_workspace.me.access_url}/icon/<path>\"`.",
96-
ForceNew: true,
97-
Optional: true,
98-
ValidateFunc: func(i any, s string) ([]string, []error) {
99-
_, err := url.Parse(s)
100-
if err != nil {
101-
return nil, []error{err}
102-
}
103-
return nil, nil
104-
},
97+
ForceNew: true,
98+
Optional: true,
99+
ValidateFunc: helpers.ValidateURL,
105100
},
106101
"slug": {
107102
Type: schema.TypeString,

provider/app_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,4 +478,61 @@ func TestApp(t *testing.T) {
478478
})
479479
}
480480
})
481+
482+
t.Run("Icon", func(t *testing.T) {
483+
t.Parallel()
484+
485+
cases := []struct {
486+
name string
487+
icon string
488+
expectError *regexp.Regexp
489+
}{
490+
{
491+
name: "Empty",
492+
icon: "",
493+
},
494+
{
495+
name: "ValidURL",
496+
icon: "/icon/region.svg",
497+
},
498+
{
499+
name: "InvalidURL",
500+
icon: "/icon%.svg",
501+
expectError: regexp.MustCompile("invalid URL escape"),
502+
},
503+
}
504+
505+
for _, c := range cases {
506+
c := c
507+
508+
t.Run(c.name, func(t *testing.T) {
509+
t.Parallel()
510+
511+
config := fmt.Sprintf(`
512+
provider "coder" {}
513+
resource "coder_agent" "dev" {
514+
os = "linux"
515+
arch = "amd64"
516+
}
517+
resource "coder_app" "code-server" {
518+
agent_id = coder_agent.dev.id
519+
slug = "code-server"
520+
display_name = "Testing"
521+
url = "http://localhost:13337"
522+
open_in = "slim-window"
523+
icon = "%s"
524+
}
525+
`, c.icon)
526+
527+
resource.Test(t, resource.TestCase{
528+
ProviderFactories: coderFactory(),
529+
IsUnitTest: true,
530+
Steps: []resource.TestStep{{
531+
Config: config,
532+
ExpectError: c.expectError,
533+
}},
534+
})
535+
})
536+
}
537+
})
481538
}

provider/helpers/validation.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package helpers
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
)
7+
8+
// ValidateURL validates that value is a valid URL string.
9+
// Accepts empty strings, local file paths, file:// URLs, and http/https URLs.
10+
// Example: for `icon = "/icon/region.svg"`, value is `/icon/region.svg` and label is `icon`.
11+
func ValidateURL(value any, label string) ([]string, []error) {
12+
val, ok := value.(string)
13+
if !ok {
14+
return nil, []error{fmt.Errorf("expected %q to be a string", label)}
15+
}
16+
17+
if _, err := url.Parse(val); err != nil {
18+
return nil, []error{err}
19+
}
20+
21+
return nil, nil
22+
}

provider/helpers/validation_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package helpers
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestValidateURL(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
value any
13+
label string
14+
expectError bool
15+
errorContains string
16+
}{
17+
// Valid cases
18+
{
19+
name: "empty string",
20+
value: "",
21+
label: "url",
22+
expectError: false,
23+
},
24+
{
25+
name: "valid http URL",
26+
value: "http://example.com",
27+
label: "url",
28+
expectError: false,
29+
},
30+
{
31+
name: "valid https URL",
32+
value: "https://example.com/path",
33+
label: "url",
34+
expectError: false,
35+
},
36+
{
37+
name: "absolute file path",
38+
value: "/path/to/file",
39+
label: "url",
40+
expectError: false,
41+
},
42+
{
43+
name: "relative file path",
44+
value: "./file.txt",
45+
label: "url",
46+
expectError: false,
47+
},
48+
{
49+
name: "relative path up directory",
50+
value: "../config.json",
51+
label: "url",
52+
expectError: false,
53+
},
54+
{
55+
name: "simple filename",
56+
value: "file.txt",
57+
label: "url",
58+
expectError: false,
59+
},
60+
{
61+
name: "URL with query params",
62+
value: "https://example.com/search?q=test",
63+
label: "url",
64+
expectError: false,
65+
},
66+
{
67+
name: "URL with fragment",
68+
value: "https://example.com/page#section",
69+
label: "url",
70+
expectError: false,
71+
},
72+
73+
// Various URL schemes that url.Parse accepts
74+
{
75+
name: "file URL scheme",
76+
value: "file:///path/to/file",
77+
label: "url",
78+
expectError: false,
79+
},
80+
{
81+
name: "ftp scheme",
82+
value: "ftp://files.example.com/file.txt",
83+
label: "url",
84+
expectError: false,
85+
},
86+
{
87+
name: "mailto scheme",
88+
value: "mailto:user@example.com",
89+
label: "url",
90+
expectError: false,
91+
},
92+
{
93+
name: "tel scheme",
94+
value: "tel:+1234567890",
95+
label: "url",
96+
expectError: false,
97+
},
98+
{
99+
name: "data scheme",
100+
value: "data:text/plain;base64,SGVsbG8=",
101+
label: "url",
102+
expectError: false,
103+
},
104+
105+
// Invalid cases
106+
{
107+
name: "non-string type - int",
108+
value: 123,
109+
label: "url",
110+
expectError: true,
111+
errorContains: "expected \"url\" to be a string",
112+
},
113+
{
114+
name: "non-string type - nil",
115+
value: nil,
116+
label: "config_url",
117+
expectError: true,
118+
errorContains: "expected \"config_url\" to be a string",
119+
},
120+
{
121+
name: "invalid URL with spaces",
122+
value: "http://example .com",
123+
label: "url",
124+
expectError: true,
125+
errorContains: "invalid character",
126+
},
127+
{
128+
name: "malformed URL",
129+
value: "http://[::1:80",
130+
label: "endpoint",
131+
expectError: true,
132+
errorContains: "missing ']'",
133+
},
134+
}
135+
136+
for _, tt := range tests {
137+
t.Run(tt.name, func(t *testing.T) {
138+
warnings, errors := ValidateURL(tt.value, tt.label)
139+
140+
if tt.expectError {
141+
require.Len(t, errors, 1, "expected an error but got none")
142+
require.Contains(t, errors[0].Error(), tt.errorContains)
143+
} else {
144+
require.Empty(t, errors, "expected no errors but got: %v", errors)
145+
}
146+
147+
// Should always return nil for warnings
148+
require.Nil(t, warnings, "expected warnings to be nil but got: %v", warnings)
149+
})
150+
}
151+
}

provider/metadata.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package provider
22

33
import (
44
"context"
5-
"net/url"
65

76
"github.com/google/uuid"
87
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
98
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
109
"golang.org/x/xerrors"
10+
11+
"github.com/coder/terraform-provider-coder/v2/provider/helpers"
1112
)
1213

1314
func metadataResource() *schema.Resource {
@@ -56,15 +57,9 @@ func metadataResource() *schema.Resource {
5657
Description: "A URL to an icon that will display in the dashboard. View built-in " +
5758
"icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " +
5859
"built-in icon with `\"${data.coder_workspace.me.access_url}/icon/<path>\"`.",
59-
ForceNew: true,
60-
Optional: true,
61-
ValidateFunc: func(i interface{}, s string) ([]string, []error) {
62-
_, err := url.Parse(s)
63-
if err != nil {
64-
return nil, []error{err}
65-
}
66-
return nil, nil
67-
},
60+
ForceNew: true,
61+
Optional: true,
62+
ValidateFunc: helpers.ValidateURL,
6863
},
6964
"daily_cost": {
7065
Type: schema.TypeInt,

provider/parameter.go

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/hex"
77
"encoding/json"
88
"fmt"
9-
"net/url"
109
"os"
1110
"regexp"
1211
"strconv"
@@ -19,6 +18,8 @@ import (
1918
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
2019
"github.com/mitchellh/mapstructure"
2120
"golang.org/x/xerrors"
21+
22+
"github.com/coder/terraform-provider-coder/v2/provider/helpers"
2223
)
2324

2425
var (
@@ -223,15 +224,9 @@ func parameterDataSource() *schema.Resource {
223224
Description: "A URL to an icon that will display in the dashboard. View built-in " +
224225
"icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " +
225226
"built-in icon with `\"${data.coder_workspace.me.access_url}/icon/<path>\"`.",
226-
ForceNew: true,
227-
Optional: true,
228-
ValidateFunc: func(i interface{}, s string) ([]string, []error) {
229-
_, err := url.Parse(s)
230-
if err != nil {
231-
return nil, []error{err}
232-
}
233-
return nil, nil
234-
},
227+
ForceNew: true,
228+
Optional: true,
229+
ValidateFunc: helpers.ValidateURL,
235230
},
236231
"option": {
237232
Type: schema.TypeList,
@@ -263,15 +258,9 @@ func parameterDataSource() *schema.Resource {
263258
Description: "A URL to an icon that will display in the dashboard. View built-in " +
264259
"icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a " +
265260
"built-in icon with `\"${data.coder_workspace.me.access_url}/icon/<path>\"`.",
266-
ForceNew: true,
267-
Optional: true,
268-
ValidateFunc: func(i interface{}, s string) ([]string, []error) {
269-
_, err := url.Parse(s)
270-
if err != nil {
271-
return nil, []error{err}
272-
}
273-
return nil, nil
274-
},
261+
ForceNew: true,
262+
Optional: true,
263+
ValidateFunc: helpers.ValidateURL,
275264
},
276265
},
277266
},

provider/parameter_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,33 @@ data "coder_parameter" "region" {
665665
}
666666
`,
667667
ExpectError: regexp.MustCompile("ephemeral parameter requires the default property"),
668-
}} {
668+
}, {
669+
Name: "InvalidIconURL",
670+
Config: `
671+
data "coder_parameter" "region" {
672+
name = "Region"
673+
type = "string"
674+
icon = "/icon%.svg"
675+
}
676+
`,
677+
ExpectError: regexp.MustCompile("invalid URL escape"),
678+
}, {
679+
Name: "OptionInvalidIconURL",
680+
Config: `
681+
data "coder_parameter" "region" {
682+
name = "Region"
683+
type = "string"
684+
option {
685+
name = "1"
686+
value = "1"
687+
icon = "/icon%.svg"
688+
description = "Something!"
689+
}
690+
}
691+
`,
692+
ExpectError: regexp.MustCompile("invalid URL escape"),
693+
},
694+
} {
669695
tc := tc
670696
t.Run(tc.Name, func(t *testing.T) {
671697
t.Parallel()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy