1
2
3
4
5 package webdav
6
7 import (
8 "context"
9 "errors"
10 "fmt"
11 "io"
12 "net/http"
13 "net/http/httptest"
14 "net/url"
15 "os"
16 "reflect"
17 "regexp"
18 "sort"
19 "strings"
20 "testing"
21 )
22
23
24 func TestPrefix(t *testing.T) {
25 const dst, blah = "Destination", "blah blah blah"
26
27
28 const createLockBody = `<?xml version="1.0" encoding="utf-8" ?>
29 <D:lockinfo xmlns:D='DAV:'>
30 <D:lockscope><D:exclusive/></D:lockscope>
31 <D:locktype><D:write/></D:locktype>
32 <D:owner>
33 <D:href>http://example.org/~ejw/contact.html</D:href>
34 </D:owner>
35 </D:lockinfo>
36 `
37
38 do := func(method, urlStr string, body string, wantStatusCode int, headers ...string) (http.Header, error) {
39 var bodyReader io.Reader
40 if body != "" {
41 bodyReader = strings.NewReader(body)
42 }
43 req, err := http.NewRequest(method, urlStr, bodyReader)
44 if err != nil {
45 return nil, err
46 }
47 for len(headers) >= 2 {
48 req.Header.Add(headers[0], headers[1])
49 headers = headers[2:]
50 }
51 res, err := http.DefaultTransport.RoundTrip(req)
52 if err != nil {
53 return nil, err
54 }
55 defer res.Body.Close()
56 if res.StatusCode != wantStatusCode {
57 return nil, fmt.Errorf("got status code %d, want %d", res.StatusCode, wantStatusCode)
58 }
59 return res.Header, nil
60 }
61
62 prefixes := []string{
63 "/",
64 "/a/",
65 "/a/b/",
66 "/a/b/c/",
67 }
68 ctx := context.Background()
69 for _, prefix := range prefixes {
70 fs := NewMemFS()
71 h := &Handler{
72 FileSystem: fs,
73 LockSystem: NewMemLS(),
74 }
75 mux := http.NewServeMux()
76 if prefix != "/" {
77 h.Prefix = prefix
78 }
79 mux.Handle(prefix, h)
80 srv := httptest.NewServer(mux)
81 defer srv.Close()
82
83
84
85
86
87
88
89
90
91
92
93
94
95 wantA := map[string]int{
96 "/": http.StatusCreated,
97 "/a/": http.StatusMovedPermanently,
98 "/a/b/": http.StatusNotFound,
99 "/a/b/c/": http.StatusNotFound,
100 }[prefix]
101 if _, err := do("MKCOL", srv.URL+"/a", "", wantA); err != nil {
102 t.Errorf("prefix=%-9q MKCOL /a: %v", prefix, err)
103 continue
104 }
105
106 wantB := map[string]int{
107 "/": http.StatusCreated,
108 "/a/": http.StatusCreated,
109 "/a/b/": http.StatusMovedPermanently,
110 "/a/b/c/": http.StatusNotFound,
111 }[prefix]
112 if _, err := do("MKCOL", srv.URL+"/a/b", "", wantB); err != nil {
113 t.Errorf("prefix=%-9q MKCOL /a/b: %v", prefix, err)
114 continue
115 }
116
117 wantC := map[string]int{
118 "/": http.StatusCreated,
119 "/a/": http.StatusCreated,
120 "/a/b/": http.StatusCreated,
121 "/a/b/c/": http.StatusMovedPermanently,
122 }[prefix]
123 if _, err := do("PUT", srv.URL+"/a/b/c", blah, wantC); err != nil {
124 t.Errorf("prefix=%-9q PUT /a/b/c: %v", prefix, err)
125 continue
126 }
127
128 wantD := map[string]int{
129 "/": http.StatusCreated,
130 "/a/": http.StatusCreated,
131 "/a/b/": http.StatusCreated,
132 "/a/b/c/": http.StatusMovedPermanently,
133 }[prefix]
134 if _, err := do("COPY", srv.URL+"/a/b/c", "", wantD, dst, srv.URL+"/a/b/d"); err != nil {
135 t.Errorf("prefix=%-9q COPY /a/b/c /a/b/d: %v", prefix, err)
136 continue
137 }
138
139 wantE := map[string]int{
140 "/": http.StatusCreated,
141 "/a/": http.StatusCreated,
142 "/a/b/": http.StatusCreated,
143 "/a/b/c/": http.StatusNotFound,
144 }[prefix]
145 if _, err := do("MKCOL", srv.URL+"/a/b/e", "", wantE); err != nil {
146 t.Errorf("prefix=%-9q MKCOL /a/b/e: %v", prefix, err)
147 continue
148 }
149
150 wantF := map[string]int{
151 "/": http.StatusCreated,
152 "/a/": http.StatusCreated,
153 "/a/b/": http.StatusCreated,
154 "/a/b/c/": http.StatusNotFound,
155 }[prefix]
156 if _, err := do("MOVE", srv.URL+"/a/b/d", "", wantF, dst, srv.URL+"/a/b/e/f"); err != nil {
157 t.Errorf("prefix=%-9q MOVE /a/b/d /a/b/e/f: %v", prefix, err)
158 continue
159 }
160
161 var lockToken string
162 wantG := map[string]int{
163 "/": http.StatusCreated,
164 "/a/": http.StatusCreated,
165 "/a/b/": http.StatusCreated,
166 "/a/b/c/": http.StatusNotFound,
167 }[prefix]
168 if h, err := do("LOCK", srv.URL+"/a/b/e/g", createLockBody, wantG); err != nil {
169 t.Errorf("prefix=%-9q LOCK /a/b/e/g: %v", prefix, err)
170 continue
171 } else {
172 lockToken = h.Get("Lock-Token")
173 }
174
175 ifHeader := fmt.Sprintf("<%s/a/b/e/g> (%s)", srv.URL, lockToken)
176 wantH := map[string]int{
177 "/": http.StatusCreated,
178 "/a/": http.StatusCreated,
179 "/a/b/": http.StatusCreated,
180 "/a/b/c/": http.StatusNotFound,
181 }[prefix]
182 if _, err := do("PUT", srv.URL+"/a/b/e/g", blah, wantH, "If", ifHeader); err != nil {
183 t.Errorf("prefix=%-9q PUT /a/b/e/g: %v", prefix, err)
184 continue
185 }
186
187 got, err := find(ctx, nil, fs, "/")
188 if err != nil {
189 t.Errorf("prefix=%-9q find: %v", prefix, err)
190 continue
191 }
192 sort.Strings(got)
193 want := map[string][]string{
194 "/": {"/", "/a", "/a/b", "/a/b/c", "/a/b/e", "/a/b/e/f", "/a/b/e/g"},
195 "/a/": {"/", "/b", "/b/c", "/b/e", "/b/e/f", "/b/e/g"},
196 "/a/b/": {"/", "/c", "/e", "/e/f", "/e/g"},
197 "/a/b/c/": {"/"},
198 }[prefix]
199 if !reflect.DeepEqual(got, want) {
200 t.Errorf("prefix=%-9q find:\ngot %v\nwant %v", prefix, got, want)
201 continue
202 }
203 }
204 }
205
206 func TestEscapeXML(t *testing.T) {
207
208
209
210
211
212
213 testCases := map[string]string{
214 "": "",
215 " ": " ",
216 "&": "&",
217 "*": "*",
218 "+": "+",
219 ",": ",",
220 "-": "-",
221 ".": ".",
222 "/": "/",
223 "0": "0",
224 "9": "9",
225 ":": ":",
226 "<": "<",
227 ">": ">",
228 "A": "A",
229 "_": "_",
230 "a": "a",
231 "~": "~",
232 "\u0201": "\u0201",
233 "&": "&amp;",
234 "foo&<b/ar>baz": "foo&<b/ar>baz",
235 }
236
237 for in, want := range testCases {
238 if got := escapeXML(in); got != want {
239 t.Errorf("in=%q: got %q, want %q", in, got, want)
240 }
241 }
242 }
243
244 func TestFilenameEscape(t *testing.T) {
245 hrefRe := regexp.MustCompile(`<D:href>([^<]*)</D:href>`)
246 displayNameRe := regexp.MustCompile(`<D:displayname>([^<]*)</D:displayname>`)
247 do := func(method, urlStr string) (string, string, error) {
248 req, err := http.NewRequest(method, urlStr, nil)
249 if err != nil {
250 return "", "", err
251 }
252 res, err := http.DefaultClient.Do(req)
253 if err != nil {
254 return "", "", err
255 }
256 defer res.Body.Close()
257
258 b, err := io.ReadAll(res.Body)
259 if err != nil {
260 return "", "", err
261 }
262 hrefMatch := hrefRe.FindStringSubmatch(string(b))
263 if len(hrefMatch) != 2 {
264 return "", "", errors.New("D:href not found")
265 }
266 displayNameMatch := displayNameRe.FindStringSubmatch(string(b))
267 if len(displayNameMatch) != 2 {
268 return "", "", errors.New("D:displayname not found")
269 }
270
271 return hrefMatch[1], displayNameMatch[1], nil
272 }
273
274 testCases := []struct {
275 name, wantHref, wantDisplayName string
276 }{{
277 name: `/foo%bar`,
278 wantHref: `/foo%25bar`,
279 wantDisplayName: `foo%bar`,
280 }, {
281 name: `/こんにちわ世界`,
282 wantHref: `/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%82%8F%E4%B8%96%E7%95%8C`,
283 wantDisplayName: `こんにちわ世界`,
284 }, {
285 name: `/Program Files/`,
286 wantHref: `/Program%20Files/`,
287 wantDisplayName: `Program Files`,
288 }, {
289 name: `/go+lang`,
290 wantHref: `/go+lang`,
291 wantDisplayName: `go+lang`,
292 }, {
293 name: `/go&lang`,
294 wantHref: `/go&lang`,
295 wantDisplayName: `go&lang`,
296 }, {
297 name: `/go<lang`,
298 wantHref: `/go%3Clang`,
299 wantDisplayName: `go<lang`,
300 }, {
301 name: `/`,
302 wantHref: `/`,
303 wantDisplayName: ``,
304 }}
305 ctx := context.Background()
306 fs := NewMemFS()
307 for _, tc := range testCases {
308 if tc.name != "/" {
309 if strings.HasSuffix(tc.name, "/") {
310 if err := fs.Mkdir(ctx, tc.name, 0755); err != nil {
311 t.Fatalf("name=%q: Mkdir: %v", tc.name, err)
312 }
313 } else {
314 f, err := fs.OpenFile(ctx, tc.name, os.O_CREATE, 0644)
315 if err != nil {
316 t.Fatalf("name=%q: OpenFile: %v", tc.name, err)
317 }
318 f.Close()
319 }
320 }
321 }
322
323 srv := httptest.NewServer(&Handler{
324 FileSystem: fs,
325 LockSystem: NewMemLS(),
326 })
327 defer srv.Close()
328
329 u, err := url.Parse(srv.URL)
330 if err != nil {
331 t.Fatal(err)
332 }
333
334 for _, tc := range testCases {
335 u.Path = tc.name
336 gotHref, gotDisplayName, err := do("PROPFIND", u.String())
337 if err != nil {
338 t.Errorf("name=%q: PROPFIND: %v", tc.name, err)
339 continue
340 }
341 if gotHref != tc.wantHref {
342 t.Errorf("name=%q: got href %q, want %q", tc.name, gotHref, tc.wantHref)
343 }
344 if gotDisplayName != tc.wantDisplayName {
345 t.Errorf("name=%q: got dispayname %q, want %q", tc.name, gotDisplayName, tc.wantDisplayName)
346 }
347 }
348 }
349
350 func TestPutRequest(t *testing.T) {
351 h := &Handler{
352 FileSystem: NewMemFS(),
353 LockSystem: NewMemLS(),
354 }
355 srv := httptest.NewServer(h)
356 defer srv.Close()
357
358 do := func(method, urlStr string, body string) (*http.Response, error) {
359 bodyReader := strings.NewReader(body)
360 req, err := http.NewRequest(method, urlStr, bodyReader)
361 if err != nil {
362 return nil, err
363 }
364 res, err := http.DefaultClient.Do(req)
365 if err != nil {
366 return nil, err
367 }
368 return res, nil
369 }
370
371 testCases := []struct {
372 name string
373 urlPrefix string
374 want int
375 }{{
376 name: "put",
377 urlPrefix: "/res",
378 want: http.StatusCreated,
379 }, {
380 name: "put_utf8_segment",
381 urlPrefix: "/res-%e2%82%ac",
382 want: http.StatusCreated,
383 }, {
384 name: "put_empty_segment",
385 urlPrefix: "",
386 want: http.StatusNotFound,
387 }, {
388 name: "put_root_segment",
389 urlPrefix: "/",
390 want: http.StatusNotFound,
391 }, {
392 name: "put_no_parent [RFC4918:S9.7.1]",
393 urlPrefix: "/409me/noparent.txt",
394 want: http.StatusConflict,
395 }}
396
397 for _, tc := range testCases {
398 urlStr := srv.URL + tc.urlPrefix
399 res, err := do("PUT", urlStr, "ABC\n")
400 if err != nil {
401 t.Errorf("name=%q: PUT: %v", tc.name, err)
402 continue
403 }
404 if res.StatusCode != tc.want {
405 t.Errorf("name=%q: got status code %d, want %d", tc.name, res.StatusCode, tc.want)
406 }
407 }
408 }
409
View as plain text