1package url
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7)
8
9func TestNormalize(t *testing.T) {
10 tests := []struct {
11 name string
12 input string
13 want string
14 }{
15 {
16 name: "empty string",
17 input: "",
18 want: "",
19 },
20 {
21 name: "http scheme unchanged",
22 input: "http://example.com",
23 want: "http://example.com",
24 },
25 {
26 name: "https scheme unchanged",
27 input: "https://example.com",
28 want: "https://example.com",
29 },
30 {
31 name: "file scheme unchanged",
32 input: "file:///path/to/file.html",
33 want: "file:///path/to/file.html",
34 },
35 {
36 name: "dumb scheme unchanged",
37 input: "dumb://home",
38 want: "dumb://home",
39 },
40 {
41 name: "about scheme unchanged",
42 input: "about:blank",
43 want: "about:blank",
44 },
45 {
46 name: "domain gets https",
47 input: "example.com",
48 want: "https://example.com",
49 },
50 {
51 name: "domain with path gets https",
52 input: "example.com/path",
53 want: "https://example.com/path",
54 },
55 {
56 name: "search query unchanged",
57 input: "hello world",
58 want: "hello world",
59 },
60 {
61 name: "single word unchanged",
62 input: "hello",
63 want: "hello",
64 },
65 {
66 name: "localhost",
67 input: "localhost",
68 want: "http://localhost",
69 },
70 {
71 name: "localhost with port",
72 input: "localhost:5173",
73 want: "http://localhost:5173",
74 },
75 {
76 name: "localhost with alt port",
77 input: "localhost:8080",
78 want: "http://localhost:8080",
79 },
80 {
81 name: "localhost with path",
82 input: "localhost/api/v1",
83 want: "http://localhost/api/v1",
84 },
85 {
86 name: "localhost.com is a domain not localhost",
87 input: "localhost.com",
88 want: "https://localhost.com",
89 },
90 {
91 name: "localhostevil.com is a domain not localhost",
92 input: "localhostevil.com",
93 want: "https://localhostevil.com",
94 },
95 }
96
97 for _, tt := range tests {
98 t.Run(tt.name, func(t *testing.T) {
99 got := Normalize(tt.input)
100 if got != tt.want {
101 t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
102 }
103 })
104 }
105}
106
107func TestNormalize_LocalFiles(t *testing.T) {
108 // Create a temporary file for testing
109 tmpDir := t.TempDir()
110 tmpFile := filepath.Join(tmpDir, "test.html")
111 if err := os.WriteFile(tmpFile, []byte("<html></html>"), 0644); err != nil {
112 t.Fatalf("failed to create temp file: %v", err)
113 }
114
115 // Create a subdirectory with a file
116 subDir := filepath.Join(tmpDir, "subdir")
117 if err := os.Mkdir(subDir, 0755); err != nil {
118 t.Fatalf("failed to create subdir: %v", err)
119 }
120 subFile := filepath.Join(subDir, "page.html")
121 if err := os.WriteFile(subFile, []byte("<html></html>"), 0644); err != nil {
122 t.Fatalf("failed to create subdir file: %v", err)
123 }
124
125 tests := []struct {
126 name string
127 input string
128 want string
129 }{
130 {
131 name: "absolute path to existing file",
132 input: tmpFile,
133 want: "file://" + tmpFile,
134 },
135 {
136 name: "absolute path to existing directory",
137 input: tmpDir,
138 want: "file://" + tmpDir,
139 },
140 {
141 name: "non-existent absolute path returned unchanged",
142 input: "/nonexistent/file.html",
143 want: "/nonexistent/file.html",
144 },
145 {
146 name: "non-existent relative path with dot prefix returned unchanged",
147 input: "./nonexistent/file.html",
148 want: "./nonexistent/file.html",
149 },
150 {
151 name: "non-existent home path returned unchanged",
152 input: "~/nonexistent/file.html",
153 want: "~/nonexistent/file.html",
154 },
155 }
156
157 for _, tt := range tests {
158 t.Run(tt.name, func(t *testing.T) {
159 got := Normalize(tt.input)
160 if got != tt.want {
161 t.Errorf("Normalize(%q) = %q, want %q", tt.input, got, tt.want)
162 }
163 })
164 }
165}
166
167func TestNormalize_RelativePaths(t *testing.T) {
168 // Create a temp file in current directory
169 tmpFile, err := os.CreateTemp(".", "test-*.html")
170 if err != nil {
171 t.Fatalf("failed to create temp file: %v", err)
172 }
173 tmpFile.Close()
174 defer os.Remove(tmpFile.Name())
175
176 // Get absolute path for comparison
177 absPath, err := filepath.Abs(tmpFile.Name())
178 if err != nil {
179 t.Fatalf("failed to get abs path: %v", err)
180 }
181
182 t.Run("relative path to existing file", func(t *testing.T) {
183 got := Normalize(tmpFile.Name())
184 want := "file://" + absPath
185 if got != want {
186 t.Errorf("Normalize(%q) = %q, want %q", tmpFile.Name(), got, want)
187 }
188 })
189}
190
191func TestExpandHome(t *testing.T) {
192 home, err := os.UserHomeDir()
193 if err != nil {
194 t.Skip("cannot get home directory")
195 }
196
197 tests := []struct {
198 name string
199 input string
200 want string
201 }{
202 {
203 name: "tilde expansion",
204 input: "~/Documents",
205 want: filepath.Join(home, "Documents"),
206 },
207 {
208 name: "no tilde unchanged",
209 input: "/absolute/path",
210 want: "/absolute/path",
211 },
212 {
213 name: "relative path unchanged",
214 input: "./relative/path",
215 want: "./relative/path",
216 },
217 {
218 name: "tilde in middle unchanged",
219 input: "/path/~/file",
220 want: "/path/~/file",
221 },
222 }
223
224 for _, tt := range tests {
225 t.Run(tt.name, func(t *testing.T) {
226 got := expandHome(tt.input)
227 if got != tt.want {
228 t.Errorf("expandHome(%q) = %q, want %q", tt.input, got, tt.want)
229 }
230 })
231 }
232}
233
234func TestLooksLikeURL(t *testing.T) {
235 tests := []struct {
236 name string
237 input string
238 want bool
239 }{
240 {
241 name: "empty string",
242 input: "",
243 want: false,
244 },
245 {
246 name: "http URL",
247 input: "http://example.com",
248 want: true,
249 },
250 {
251 name: "https URL",
252 input: "https://example.com",
253 want: true,
254 },
255 {
256 name: "file URL",
257 input: "file:///path/to/file",
258 want: true,
259 },
260 {
261 name: "dumb URL",
262 input: "dumb://home",
263 want: true,
264 },
265 {
266 name: "about URL",
267 input: "about:blank",
268 want: true,
269 },
270 {
271 name: "domain",
272 input: "example.com",
273 want: true,
274 },
275 {
276 name: "domain with path",
277 input: "example.com/path",
278 want: true,
279 },
280 {
281 name: "search query",
282 input: "hello world",
283 want: false,
284 },
285 {
286 name: "single word",
287 input: "hello",
288 want: false,
289 },
290 {
291 name: "localhost",
292 input: "localhost",
293 want: true,
294 },
295 {
296 name: "localhost with port",
297 input: "localhost:5173",
298 want: true,
299 },
300 {
301 name: "localhost with alt port",
302 input: "localhost:8080",
303 want: true,
304 },
305 {
306 name: "localhost with path",
307 input: "localhost/api/v1",
308 want: true,
309 },
310 {
311 name: "localhost.com is a domain not localhost",
312 input: "localhost.com",
313 want: true,
314 },
315 {
316 name: "localhostevil.com is a domain not localhost",
317 input: "localhostevil.com",
318 want: true,
319 },
320 }
321
322 for _, tt := range tests {
323 t.Run(tt.name, func(t *testing.T) {
324 got := LooksLikeURL(tt.input)
325 if got != tt.want {
326 t.Errorf("LooksLikeURL(%q) = %v, want %v", tt.input, got, tt.want)
327 }
328 })
329 }
330}