// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package webdav
import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"reflect"
	"sort"
	"strings"
	"testing"
	ixml "github.com/alist-org/alist/v3/server/webdav/internal/xml"
)
func TestReadLockInfo(t *testing.T) {
	// The "section x.y.z" test cases come from section x.y.z of the spec at
	// http://www.webdav.org/specs/rfc4918.html
	testCases := []struct {
		desc       string
		input      string
		wantLI     lockInfo
		wantStatus int
	}{{
		"bad: junk",
		"xxx",
		lockInfo{},
		http.StatusBadRequest,
	}, {
		"bad: invalid owner XML",
		"" +
			"\n" +
			"  \n" +
			"  \n" +
			"  \n" +
			"       no end tag   \n" +
			"  \n" +
			"",
		lockInfo{},
		http.StatusBadRequest,
	}, {
		"bad: invalid UTF-8",
		"" +
			"\n" +
			"  \n" +
			"  \n" +
			"  \n" +
			"       \xff   \n" +
			"  \n" +
			"",
		lockInfo{},
		http.StatusBadRequest,
	}, {
		"bad: unfinished XML #1",
		"" +
			"\n" +
			"  \n" +
			"  \n",
		lockInfo{},
		http.StatusBadRequest,
	}, {
		"bad: unfinished XML #2",
		"" +
			"\n" +
			"  \n" +
			"  \n" +
			"  \n",
		lockInfo{},
		http.StatusBadRequest,
	}, {
		"good: empty",
		"",
		lockInfo{},
		0,
	}, {
		"good: plain-text owner",
		"" +
			"\n" +
			"  \n" +
			"  \n" +
			"  gopher\n" +
			"",
		lockInfo{
			XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
			Exclusive: new(struct{}),
			Write:     new(struct{}),
			Owner: owner{
				InnerXML: "gopher",
			},
		},
		0,
	}, {
		"section 9.10.7",
		"" +
			"\n" +
			"  \n" +
			"  \n" +
			"  \n" +
			"    http://example.org/~ejw/contact.html\n" +
			"  \n" +
			"",
		lockInfo{
			XMLName:   ixml.Name{Space: "DAV:", Local: "lockinfo"},
			Exclusive: new(struct{}),
			Write:     new(struct{}),
			Owner: owner{
				InnerXML: "\n    http://example.org/~ejw/contact.html\n  ",
			},
		},
		0,
	}}
	for _, tc := range testCases {
		li, status, err := readLockInfo(strings.NewReader(tc.input))
		if tc.wantStatus != 0 {
			if err == nil {
				t.Errorf("%s: got nil error, want non-nil", tc.desc)
				continue
			}
		} else if err != nil {
			t.Errorf("%s: %v", tc.desc, err)
			continue
		}
		if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
			t.Errorf("%s:\ngot  lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
				tc.desc, li, status, tc.wantLI, tc.wantStatus)
			continue
		}
	}
}
func TestReadPropfind(t *testing.T) {
	testCases := []struct {
		desc       string
		input      string
		wantPF     propfind
		wantStatus int
	}{{
		desc: "propfind: propname",
		input: "" +
			"\n" +
			"  \n" +
			"",
		wantPF: propfind{
			XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
			Propname: new(struct{}),
		},
	}, {
		desc:  "propfind: empty body means allprop",
		input: "",
		wantPF: propfind{
			Allprop: new(struct{}),
		},
	}, {
		desc: "propfind: allprop",
		input: "" +
			"\n" +
			"   \n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Allprop: new(struct{}),
		},
	}, {
		desc: "propfind: allprop followed by include",
		input: "" +
			"\n" +
			"  \n" +
			"  \n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Allprop: new(struct{}),
			Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
		},
	}, {
		desc: "propfind: include followed by allprop",
		input: "" +
			"\n" +
			"  \n" +
			"  \n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Allprop: new(struct{}),
			Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
		},
	}, {
		desc: "propfind: propfind",
		input: "" +
			"\n" +
			"  \n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
		},
	}, {
		desc: "propfind: prop with ignored comments",
		input: "" +
			"\n" +
			"  \n" +
			"    \n" +
			"    \n" +
			"  \n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
		},
	}, {
		desc: "propfind: propfind with ignored whitespace",
		input: "" +
			"\n" +
			"     \n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
		},
	}, {
		desc: "propfind: propfind with ignored mixed-content",
		input: "" +
			"\n" +
			"  foobar\n" +
			"",
		wantPF: propfind{
			XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
			Prop:    propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
		},
	}, {
		desc: "propfind: propname with ignored element (section A.4)",
		input: "" +
			"\n" +
			"  \n" +
			"  *boss*\n" +
			"",
		wantPF: propfind{
			XMLName:  ixml.Name{Space: "DAV:", Local: "propfind"},
			Propname: new(struct{}),
		},
	}, {
		desc:       "propfind: bad: junk",
		input:      "xxx",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: propname and allprop (section A.3)",
		input: "" +
			"\n" +
			"  " +
			"  " +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: propname and prop",
		input: "" +
			"\n" +
			"  \n" +
			"  \n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: allprop and prop",
		input: "" +
			"\n" +
			"  \n" +
			"  \n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: empty propfind with ignored element (section A.4)",
		input: "" +
			"\n" +
			"  \n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: empty prop",
		input: "" +
			"\n" +
			"  \n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: prop with just chardata",
		input: "" +
			"\n" +
			"  foo\n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "bad: interrupted prop",
		input: "" +
			"\n" +
			"  \n",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "bad: malformed end element prop",
		input: "" +
			"\n" +
			"  \n",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: property with chardata value",
		input: "" +
			"\n" +
			"  bar\n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: property with whitespace value",
		input: "" +
			"\n" +
			"   \n" +
			"",
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "propfind: bad: include without allprop",
		input: "" +
			"\n" +
			"  \n" +
			"",
		wantStatus: http.StatusBadRequest,
	}}
	for _, tc := range testCases {
		pf, status, err := readPropfind(strings.NewReader(tc.input))
		if tc.wantStatus != 0 {
			if err == nil {
				t.Errorf("%s: got nil error, want non-nil", tc.desc)
				continue
			}
		} else if err != nil {
			t.Errorf("%s: %v", tc.desc, err)
			continue
		}
		if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
			t.Errorf("%s:\ngot  propfind=%v, status=%v\nwant propfind=%v, status=%v",
				tc.desc, pf, status, tc.wantPF, tc.wantStatus)
			continue
		}
	}
}
func TestMultistatusWriter(t *testing.T) {
	///The "section x.y.z" test cases come from section x.y.z of the spec at
	// http://www.webdav.org/specs/rfc4918.html
	testCases := []struct {
		desc        string
		responses   []response
		respdesc    string
		writeHeader bool
		wantXML     string
		wantCode    int
		wantErr     error
	}{{
		desc: "section 9.2.2 (failed dependency)",
		responses: []response{{
			Href: []string{"http://example.com/foo"},
			Propstat: []propstat{{
				Prop: []Property{{
					XMLName: xml.Name{
						Space: "http://ns.example.com/",
						Local: "Authors",
					},
				}},
				Status: "HTTP/1.1 424 Failed Dependency",
			}, {
				Prop: []Property{{
					XMLName: xml.Name{
						Space: "http://ns.example.com/",
						Local: "Copyright-Owner",
					},
				}},
				Status: "HTTP/1.1 409 Conflict",
			}},
			ResponseDescription: "Copyright Owner cannot be deleted or altered.",
		}},
		wantXML: `` +
			`` +
			`` +
			`  ` +
			`    http://example.com/foo` +
			`    ` +
			`      ` +
			`        ` +
			`      ` +
			`      HTTP/1.1 424 Failed Dependency` +
			`    ` +
			`    ` +
			`      ` +
			`        ` +
			`      ` +
			`      HTTP/1.1 409 Conflict` +
			`    ` +
			`  Copyright Owner cannot be deleted or altered.` +
			`` +
			``,
		wantCode: StatusMulti,
	}, {
		desc: "section 9.6.2 (lock-token-submitted)",
		responses: []response{{
			Href:   []string{"http://example.com/foo"},
			Status: "HTTP/1.1 423 Locked",
			Error: &xmlError{
				InnerXML: []byte(``),
			},
		}},
		wantXML: `` +
			`` +
			`` +
			`  ` +
			`    http://example.com/foo` +
			`    HTTP/1.1 423 Locked` +
			`    ` +
			`  ` +
			``,
		wantCode: StatusMulti,
	}, {
		desc: "section 9.1.3",
		responses: []response{{
			Href: []string{"http://example.com/foo"},
			Propstat: []propstat{{
				Prop: []Property{{
					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
					InnerXML: []byte(`` +
						`` +
						`Box type A` +
						``),
				}, {
					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
					InnerXML: []byte(`` +
						`` +
						`J.J. Johnson` +
						``),
				}},
				Status: "HTTP/1.1 200 OK",
			}, {
				Prop: []Property{{
					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
				}, {
					XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
				}},
				Status:              "HTTP/1.1 403 Forbidden",
				ResponseDescription: "The user does not have access to the DingALing property.",
			}},
		}},
		respdesc: "There has been an access violation error.",
		wantXML: `` +
			`` +
			`` +
			`  ` +
			`    http://example.com/foo` +
			`    ` +
			`      ` +
			`        Box type A` +
			`        J.J. Johnson` +
			`      ` +
			`      HTTP/1.1 200 OK` +
			`    ` +
			`    ` +
			`      ` +
			`        ` +
			`        ` +
			`      ` +
			`      HTTP/1.1 403 Forbidden` +
			`      The user does not have access to the DingALing property.` +
			`    ` +
			`  ` +
			`  There has been an access violation error.` +
			``,
		wantCode: StatusMulti,
	}, {
		desc: "no response written",
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}, {
		desc:     "no response written (with description)",
		respdesc: "too bad",
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}, {
		desc:        "empty multistatus with header",
		writeHeader: true,
		wantXML:     ``,
		wantCode:    StatusMulti,
	}, {
		desc: "bad: no href",
		responses: []response{{
			Propstat: []propstat{{
				Prop: []Property{{
					XMLName: xml.Name{
						Space: "http://example.com/",
						Local: "foo",
					},
				}},
				Status: "HTTP/1.1 200 OK",
			}},
		}},
		wantErr: errInvalidResponse,
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}, {
		desc: "bad: multiple hrefs and no status",
		responses: []response{{
			Href: []string{"http://example.com/foo", "http://example.com/bar"},
		}},
		wantErr: errInvalidResponse,
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}, {
		desc: "bad: one href and no propstat",
		responses: []response{{
			Href: []string{"http://example.com/foo"},
		}},
		wantErr: errInvalidResponse,
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}, {
		desc: "bad: status with one href and propstat",
		responses: []response{{
			Href: []string{"http://example.com/foo"},
			Propstat: []propstat{{
				Prop: []Property{{
					XMLName: xml.Name{
						Space: "http://example.com/",
						Local: "foo",
					},
				}},
				Status: "HTTP/1.1 200 OK",
			}},
			Status: "HTTP/1.1 200 OK",
		}},
		wantErr: errInvalidResponse,
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}, {
		desc: "bad: multiple hrefs and propstat",
		responses: []response{{
			Href: []string{
				"http://example.com/foo",
				"http://example.com/bar",
			},
			Propstat: []propstat{{
				Prop: []Property{{
					XMLName: xml.Name{
						Space: "http://example.com/",
						Local: "foo",
					},
				}},
				Status: "HTTP/1.1 200 OK",
			}},
		}},
		wantErr: errInvalidResponse,
		// default of http.responseWriter
		wantCode: http.StatusOK,
	}}
	n := xmlNormalizer{omitWhitespace: true}
loop:
	for _, tc := range testCases {
		rec := httptest.NewRecorder()
		w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
		if tc.writeHeader {
			if err := w.writeHeader(); err != nil {
				t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
				continue
			}
		}
		for _, r := range tc.responses {
			if err := w.write(&r); err != nil {
				if err != tc.wantErr {
					t.Errorf("%s: got write error %v, want %v",
						tc.desc, err, tc.wantErr)
				}
				continue loop
			}
		}
		if err := w.close(); err != tc.wantErr {
			t.Errorf("%s: got close error %v, want %v",
				tc.desc, err, tc.wantErr)
			continue
		}
		if rec.Code != tc.wantCode {
			t.Errorf("%s: got HTTP status code %d, want %d\n",
				tc.desc, rec.Code, tc.wantCode)
			continue
		}
		gotXML := rec.Body.String()
		eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
		if err != nil {
			t.Errorf("%s: equalXML: %v", tc.desc, err)
			continue
		}
		if !eq {
			t.Errorf("%s: XML body\ngot  %s\nwant %s", tc.desc, gotXML, tc.wantXML)
		}
	}
}
func TestReadProppatch(t *testing.T) {
	ppStr := func(pps []Proppatch) string {
		var outer []string
		for _, pp := range pps {
			var inner []string
			for _, p := range pp.Props {
				inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}",
					p.XMLName, p.Lang, p.InnerXML))
			}
			outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
				pp.Remove, strings.Join(inner, ", ")))
		}
		return "[" + strings.Join(outer, ", ") + "]"
	}
	testCases := []struct {
		desc       string
		input      string
		wantPP     []Proppatch
		wantStatus int
	}{{
		desc: "proppatch: section 9.2 (with simple property value)",
		input: `` +
			`` +
			`` +
			`    ` +
			`         somevalue` +
			`    ` +
			`    ` +
			`         ` +
			`    ` +
			``,
		wantPP: []Proppatch{{
			Props: []Property{{
				xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
				"",
				[]byte(`somevalue`),
			}},
		}, {
			Remove: true,
			Props: []Property{{
				xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
				"",
				nil,
			}},
		}},
	}, {
		desc: "proppatch: lang attribute on prop",
		input: `` +
			`` +
			`` +
			`    ` +
			`         ` +
			`              ` +
			`         ` +
			`    ` +
			``,
		wantPP: []Proppatch{{
			Props: []Property{{
				xml.Name{Space: "http://example.com/ns", Local: "foo"},
				"en",
				nil,
			}},
		}},
	}, {
		desc: "bad: remove with value",
		input: `` +
			`` +
			`` +
			`    ` +
			`         ` +
			`              ` +
			`              Jim Whitehead` +
			`              ` +
			`         ` +
			`    ` +
			``,
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "bad: empty propertyupdate",
		input: `` +
			`` +
			``,
		wantStatus: http.StatusBadRequest,
	}, {
		desc: "bad: empty prop",
		input: `` +
			`` +
			`` +
			`    ` +
			`        ` +
			`    ` +
			``,
		wantStatus: http.StatusBadRequest,
	}}
	for _, tc := range testCases {
		pp, status, err := readProppatch(strings.NewReader(tc.input))
		if tc.wantStatus != 0 {
			if err == nil {
				t.Errorf("%s: got nil error, want non-nil", tc.desc)
				continue
			}
		} else if err != nil {
			t.Errorf("%s: %v", tc.desc, err)
			continue
		}
		if status != tc.wantStatus {
			t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
			continue
		}
		if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
			t.Errorf("%s: proppatch\ngot  %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP))
		}
	}
}
func TestUnmarshalXMLValue(t *testing.T) {
	testCases := []struct {
		desc    string
		input   string
		wantVal string
	}{{
		desc:    "simple char data",
		input:   "foo",
		wantVal: "foo",
	}, {
		desc:    "empty element",
		input:   "",
		wantVal: "",
	}, {
		desc:    "preserve namespace",
		input:   ``,
		wantVal: ``,
	}, {
		desc:    "preserve root element namespace",
		input:   ``,
		wantVal: ``,
	}, {
		desc:    "preserve whitespace",
		input:   "  \t ",
		wantVal: "  \t ",
	}, {
		desc:    "preserve mixed content",
		input:   `  a  `,
		wantVal: `  a  `,
	}, {
		desc: "section 9.2",
		input: `` +
			`` +
			`  Jim Whitehead` +
			`  Roy Fielding` +
			``,
		wantVal: `` +
			`  Jim Whitehead` +
			`  Roy Fielding`,
	}, {
		desc: "section 4.3.1 (mixed content)",
		input: `` +
			`` +
			`  Jane Doe` +
			`  ` +
			`  mailto:jane.doe@example.com` +
			`  http://www.example.com` +
			`  ` +
			`    Jane has been working way too long on the` +
			`    long-awaited revision of ]]>.` +
			`  ` +
			``,
		wantVal: `` +
			`  Jane Doe` +
			`  ` +
			`  mailto:jane.doe@example.com` +
			`  http://www.example.com` +
			`  ` +
			`    Jane has been working way too long on the` +
			`    long-awaited revision of <RFC2518>.` +
			`  `,
	}}
	var n xmlNormalizer
	for _, tc := range testCases {
		d := ixml.NewDecoder(strings.NewReader(tc.input))
		var v xmlValue
		if err := d.Decode(&v); err != nil {
			t.Errorf("%s: got error %v, want nil", tc.desc, err)
			continue
		}
		eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
		if err != nil {
			t.Errorf("%s: equalXML: %v", tc.desc, err)
			continue
		}
		if !eq {
			t.Errorf("%s:\ngot  %s\nwant %s", tc.desc, string(v), tc.wantVal)
		}
	}
}
// xmlNormalizer normalizes XML.
type xmlNormalizer struct {
	// omitWhitespace instructs to ignore whitespace between element tags.
	omitWhitespace bool
	// omitComments instructs to ignore XML comments.
	omitComments bool
}
// normalize writes the normalized XML content of r to w. It applies the
// following rules
//
//   - Rename namespace prefixes according to an internal heuristic.
//   - Remove unnecessary namespace declarations.
//   - Sort attributes in XML start elements in lexical order of their
//     fully qualified name.
//   - Remove XML directives and processing instructions.
//   - Remove CDATA between XML tags that only contains whitespace, if
//     instructed to do so.
//   - Remove comments, if instructed to do so.
func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
	d := ixml.NewDecoder(r)
	e := ixml.NewEncoder(w)
	for {
		t, err := d.Token()
		if err != nil {
			if t == nil && err == io.EOF {
				break
			}
			return err
		}
		switch val := t.(type) {
		case ixml.Directive, ixml.ProcInst:
			continue
		case ixml.Comment:
			if n.omitComments {
				continue
			}
		case ixml.CharData:
			if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
				continue
			}
		case ixml.StartElement:
			start, _ := ixml.CopyToken(val).(ixml.StartElement)
			attr := start.Attr[:0]
			for _, a := range start.Attr {
				if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
					continue
				}
				attr = append(attr, a)
			}
			sort.Sort(byName(attr))
			start.Attr = attr
			t = start
		}
		err = e.EncodeToken(t)
		if err != nil {
			return err
		}
	}
	return e.Flush()
}
// equalXML tests for equality of the normalized XML contents of a and b.
func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
	var buf bytes.Buffer
	if err := n.normalize(&buf, a); err != nil {
		return false, err
	}
	normA := buf.String()
	buf.Reset()
	if err := n.normalize(&buf, b); err != nil {
		return false, err
	}
	normB := buf.String()
	return normA == normB, nil
}
type byName []ixml.Attr
func (a byName) Len() int      { return len(a) }
func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byName) Less(i, j int) bool {
	if a[i].Name.Space != a[j].Name.Space {
		return a[i].Name.Space < a[j].Name.Space
	}
	return a[i].Name.Local < a[j].Name.Local
}