go-webfinger/http_test.go

289 lines
8.2 KiB
Go

package webfinger
import (
"bytes"
"net/http"
"net/url"
"sort"
"testing"
"reflect"
"crypto/tls"
"strings"
"github.com/pkg/errors"
)
type dummyUserResolver struct {
}
func (d *dummyUserResolver) FindUser(username string, hostname, requestHost string, rel []Rel) (*Resource, error) {
if username == "hello" {
if len(rel) == 2 && rel[0] == "x" && rel[1] == "y" {
return &Resource{
Links: []Link{
Link{
HRef: string(rel[0]),
Rel: string(rel[0]),
},
Link{
HRef: string(rel[1]),
Rel: string(rel[1]),
},
},
}, nil
}
return &Resource{
Links: []Link{
Link{
HRef: string("x"),
Rel: string("x"),
},
Link{
HRef: string("y"),
Rel: string("y"),
},
Link{
HRef: string("z"),
Rel: string("z"),
},
},
}, nil
}
return nil, errors.New("User not found")
}
func (d *dummyUserResolver) DummyUser(username string, hostname string, rel []Rel) (*Resource, error) {
return nil, errors.New("User not found")
}
func (d *dummyUserResolver) IsNotFoundError(err error) bool {
if err == nil {
return false
}
if err.Error() == "User not found" {
return true
}
if errors.Cause(err).Error() == "User not found" {
return true
}
return false
}
type dummyResponseWriter struct {
bytes.Buffer
code int
headers http.Header
}
func (d *dummyResponseWriter) Header() http.Header {
if d.headers == nil {
d.headers = make(http.Header)
}
return d.headers
}
func (d *dummyResponseWriter) WriteHeader(i int) {
d.code = i
}
type serveHTTPTest struct {
Description string
Input *http.Request
OutputCode int
OutputHeaders http.Header
OutputBody string
}
type kv struct {
key string
val string
}
func buildRequest(method string, path string, query string, kvx ...kv) *http.Request {
r := &http.Request{}
r.Host = "http://localhost"
r.Header = make(http.Header)
r.URL = &url.URL{
Path: path,
RawQuery: query,
Host: "localhost",
Scheme: "http",
}
for _, k := range kvx {
r.Header.Add(k.key, k.val)
}
r.Method = method
return r
}
func buildRequestTLS(method string, path string, query string, kvx ...kv) *http.Request {
r := &http.Request{}
r.Host = "https://localhost"
r.Header = make(http.Header)
r.URL = &url.URL{
Path: path,
RawQuery: query,
Host: "localhost",
Scheme: "https",
}
r.TLS = &tls.ConnectionState{} // marks the request as TLS
for _, k := range kvx {
r.Header.Add(k.key, k.val)
}
r.Method = method
return r
}
var defaultHeaders = http.Header(map[string][]string{
"Cache-Control": []string{"no-cache"},
"Pragma": []string{"no-cache"},
"Content-Type": []string{"application/jrd+json"},
})
func plusHeader(h http.Header, kvx ...kv) http.Header {
var h2 = make(http.Header)
for k, vx := range h {
for _, v := range vx {
h2.Add(k, v)
}
}
for _, k := range kvx {
h2.Add(k.key, k.val)
}
return h2
}
func compareHeaders(h1 http.Header, h2 http.Header) bool {
if len(h1) != len(h2) {
return false
}
keys := []string{}
for k := range h1 {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if !reflect.DeepEqual(h1[k], h2[k]) {
return false
}
}
return true
}
func TestServiceServeHTTP(t *testing.T) {
var tests = []serveHTTPTest{
{"GET root URL should return 404 not found with no headers",
buildRequestTLS("GET", "/", ""), http.StatusNotFound, make(http.Header), ""},
{"POST root URL should return 404 not found with no headers",
buildRequestTLS("POST", "/", ""), http.StatusNotFound, make(http.Header), ""},
{"GET /.well-known should return 404 not found with no headers",
buildRequestTLS("GET", "/.well-known", ""), http.StatusNotFound, make(http.Header), ""},
{"POST /.well-known should return 404 not found with no headers",
buildRequestTLS("POST", "/.well-known", ""), http.StatusNotFound, make(http.Header), ""},
/*
4.2. Performing a WebFinger Query
*/
/*
A WebFinger client issues a query using the GET method to the well-
known [3] resource identified by the URI whose path component is
"/.well-known/webfinger" and whose query component MUST include the
"resource" parameter exactly once and set to the value of the URI for
which information is being sought.
*/
{"GET Webfinger resource should return valid response with default headers",
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain"), http.StatusOK, defaultHeaders,
`{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"},{"href":"z","ref":"z"}]}`},
{"POST Webfinger URL should fail with MethodNotAllowed",
buildRequestTLS("POST", WebFingerPath, ""), http.StatusMethodNotAllowed, defaultHeaders, ""},
{"GET multiple resources should fail with BadRequest",
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain&resource=acct:hello2@domain"), http.StatusBadRequest, defaultHeaders, ""},
/*
The "rel" parameter MAY be included multiple times in order to
request multiple link relation types.
*/
{"GET Webfinger resource with rel should filter results",
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain&rel=x&rel=y"), http.StatusOK, defaultHeaders,
`{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"}]}`},
/*
A WebFinger resource MAY redirect the client; if it does, the
redirection MUST only be to an "https" URI and the client MUST
perform certificate validation again when redirected.
*/
{"GET non-TLS should redirect to TLS",
buildRequest("GET", WebFingerPath, "resource=acct:hello@domain"), http.StatusSeeOther, plusHeader(
defaultHeaders, kv{"Location", "https://localhost/.well-known/webfinger?resource=acct:hello@domain"}), ""},
/*
A WebFinger resource MUST return a JRD as the representation for the
resource if the client requests no other supported format explicitly
via the HTTP "Accept" header. The client MAY include the "Accept"
header to indicate a desired representation; representations other
than JRD might be defined in future specifications. The WebFinger
resource MUST silently ignore any requested representations that it
does not understand or support. The media type used for the JSON
Resource Descriptor (JRD) is "application/jrd+json" (see Section
10.2).
*/
{"GET with Accept should return as normal",
buildRequestTLS("GET", WebFingerPath, "resource=acct:hello@domain", kv{"Accept", "application/json"}), http.StatusOK, defaultHeaders,
`{"links":[{"href":"x","ref":"x"},{"href":"y","ref":"y"},{"href":"z","ref":"z"}]}`},
/*
If the "resource" parameter is a value for which the server has no
information, the server MUST indicate that it was unable to match the
request as per Section 10.4.5 of RFC 2616.
*/
{"GET with a missing user should return 404",
buildRequestTLS("GET", WebFingerPath, "resource=acct:missinguser@domain"), http.StatusNotFound, defaultHeaders, ""},
/*
If the "resource" parameter is absent or malformed, the WebFinger
resource MUST indicate that the request is bad as per Section 10.4.1
of RFC 2616 [2]. (400 bad request)
*/
{"GET with no resource should fail with BadRequest",
buildRequestTLS("GET", WebFingerPath, ""), http.StatusBadRequest, defaultHeaders, ""},
{"GET with malformed resource URI should fail with BadRequest",
buildRequestTLS("GET", WebFingerPath, "resource=hello-world"), http.StatusBadRequest, defaultHeaders, ""},
{"GET with http resource URI should fail with BadRequest",
buildRequestTLS("GET", WebFingerPath, "resource=http://hello-world"), http.StatusBadRequest, defaultHeaders, ""},
}
svc := Default(&dummyUserResolver{})
for _, tx := range tests {
w := &dummyResponseWriter{code: 200}
svc.ServeHTTP(w, tx.Input)
// code should be 404
// headers should be empty
body := strings.TrimSpace(string(w.Buffer.Bytes()))
failed := false
failed = failed || w.code != tx.OutputCode
failed = failed || !compareHeaders(tx.OutputHeaders, w.headers)
failed = failed || body != tx.OutputBody
if failed {
t.Errorf("%s\nHTTP '%v' '%v' => \n\t'%v'\n\t'%v'\n\t'%v';\nexpected \n\t'%v \n\t'%v' \n\t'%v'",
tx.Description,
tx.Input.Method, tx.Input.URL,
w.code, w.headers, body,
tx.OutputCode, tx.OutputHeaders, tx.OutputBody)
}
}
}