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