tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

server-manual.html (16941B)


      1 <!doctype html>
      2 <html>
      3 <head>
      4 <title>Annotation Protocol Must Tests</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 <script src="/common/utils.js"></script>
      8 <script type="application/javascript">
      9 /* globals header, assert_equals, promise_test, assert_true, uuid, assert_regexp_match */
     10 
     11 /* jshint unused: false, strict: false */
     12 
     13 setup( { explicit_timeout: true, explicit_done: true } );
     14 
     15 // just ld+json here as the full profile'd media type is a SHOULD
     16 var MEDIA_TYPE = 'application/ld+json';
     17 var MEDIA_TYPE_REGEX = /application\/ld\+json/;
     18 // a request timeout if there is not one specified in the parent window
     19 
     20 var myTimeout = 5000;
     21 
     22 function request(method, url, headers, content) {
     23  if (method === undefined) {
     24    method = "GET";
     25  }
     26 
     27  return new Promise(function (resolve, reject) {
     28    var xhr = new XMLHttpRequest();
     29 
     30    // this gets returned when the request completes
     31    var resp = {
     32      xhr: xhr,
     33      headers: null,
     34      status: 0,
     35      body: null,
     36      text: ""
     37    };
     38 
     39    xhr.open(method, url);
     40 
     41    // headers?
     42    if (headers !== undefined) {
     43      headers.forEach(function(ref, idx) {
     44        xhr.setRequestHeader(ref[0], ref[1]);
     45      });
     46    }
     47 
     48    // xhr.timeout = myTimeout;
     49 
     50    xhr.ontimeout = function() {
     51      resp.timeout = myTimeout;
     52      resolve(resp);
     53    };
     54 
     55    xhr.onerror = function() {
     56      resolve(resp);
     57    };
     58 
     59    xhr.onload = function () {
     60      resp.status = this.status;
     61      if (this.status >= 200 && this.status < 300) {
     62        var d = xhr.response;
     63        resp.text = d;
     64        // we have it; what is it?
     65        var type = xhr.getResponseHeader('Content-Type');
     66        if (type) {
     67          resp.type = type.split(';')[0];
     68          if (resp.type === MEDIA_TYPE) {
     69            try {
     70              d = JSON.parse(d);
     71              resp.body = d;
     72            }
     73            catch(err) {
     74              resp.body = null;
     75            }
     76          }
     77        } else {
     78          resp.type = null;
     79          resp.body = null;
     80        }
     81 
     82      }
     83      resolve(resp);
     84    };
     85 
     86    if (content !== undefined) {
     87      if ("object" === typeof(content)) {
     88        xhr.send(JSON.stringify(content));
     89      } else if ("function" === typeof(content)) {
     90        xhr.send(content());
     91      } else if ("string" === typeof(content)) {
     92        xhr.send(content);
     93      }
     94    } else {
     95      xhr.send();
     96    }
     97  });
     98 }
     99 
    100 function checkBody(res, pat, isRE) {
    101  if (isRE === undefined) {
    102    isRE = true;
    103  }
    104  if (!res.body) {
    105    if (isRE) {
    106      assert_regexp_match("", pat, header + " not found in body");
    107    } else {
    108      assert_equals("", pat, header + " not found in body") ;
    109    }
    110  } else {
    111    if (isRE) {
    112      assert_regexp_match(res.body, pat, pat + " not found in body ");
    113    } else {
    114      assert_equals(res.body, pat, pat + " not found in body");
    115    }
    116  }
    117 }
    118 
    119 function checkHeader(res, header, pat, isRE) {
    120  if (isRE === undefined) {
    121    isRE = true;
    122  }
    123  if (!res.xhr.getResponseHeader(header)) {
    124    if (isRE) {
    125      assert_regexp_match("", pat, header + " not found in response");
    126    } else {
    127      assert_equals("", pat, header + " not found in response") ;
    128    }
    129  } else {
    130    var val = res.xhr.getResponseHeader(header) ;
    131    if (isRE) {
    132      assert_regexp_match(val, pat, pat + " not found in " + header);
    133    } else {
    134      assert_equals(val, pat, pat + " not found in " + header);
    135    }
    136  }
    137 }
    138 
    139 /* makePromiseTests
    140 *
    141 * thennable   - Promise that when resolved will send data into the test
    142 * criteria    - Array of assertions
    143 */
    144 
    145 function makePromiseTests( thennable, criteria ) {
    146  // loop over the array of criteria
    147  //
    148  // create a promise_test for each one
    149  criteria.forEach(function(ref) {
    150    promise_test(function() {
    151      return thennable.then(function(res) {
    152        if (ref.header !== undefined) {
    153        // it is a header check
    154          if (ref.pat !== undefined) {
    155            checkHeader(res, ref.header, ref.pat, true);
    156          } else if (ref.string !== undefined) {
    157            checkHeader(res, ref.header, ref.string, false);
    158          } else if (ref.test !== undefined) {
    159            assert_true(ref.test(res));
    160          }
    161        } else {
    162          if (ref.pat !== undefined) {
    163            checkBody(res, ref.pat, true);
    164          } else if (ref.string !== undefined) {
    165            checkBody(res, ref.string, false);
    166          } else if (ref.test !== undefined) {
    167            assert_true(ref.test(res));
    168          }
    169        }
    170      });
    171    }, ref.assertion);
    172  });
    173 }
    174 
    175 function runTests( container_url, annotation_url ) {
    176  // trim whitespace from incoming variables
    177  container_url = container_url.trim();
    178  annotation_url = annotation_url.trim();
    179 
    180  // Section 4 has a requirement that the URL end in a slash, so...
    181  // ensure the url has a length
    182  test(function() {
    183    assert_regexp_match(container_url, /\/$/, 'Container URL did not end in a "/" character');
    184  }, 'Container MUST end in a "/" character');
    185 
    186  // Container tests
    187  var theContainer = request("GET", container_url);
    188 
    189  makePromiseTests( theContainer, [
    190      { header: 'Allow', pat: /GET/, assertion: "Containers MUST support GET (check Allow on GET)" },
    191      { header: 'Allow', pat: /HEAD/, assertion: "Containers MUST support HEAD (check Allow on GET)" },
    192      { header: 'Allow', pat: /OPTIONS/, assertion: "Containers MUST support OPTIONS (check Allow on GET)" },
    193      { header: 'Content-Type', pat: MEDIA_TYPE_REGEX, assertion: 'Containers MUST have a Content-Type header with the application/ld+json media type'},
    194      { header: 'Content-Type', pat: MEDIA_TYPE_REGEX, assertion: 'Containers MUST response with the JSON-LD representation (by default)'},
    195      { test: function(res) { return ( 'type' in res.body && res.body.type.indexOf('BasicContainer') > -1 ); }, assertion: 'Containers MUST return a description of the container with BasicContainer' },
    196      { test: function(res) { return ( 'type' in res.body && res.body.type.indexOf('AnnotationCollection') > -1 ); }, assertion: 'Containers MUST return a description of the container with AnnotationCollection' },
    197      { header: 'Link', pat: /(.*)/, assertion: 'Containers MUST return a Link header (rfc5988) on all responses' },
    198      { header: 'ETag', pat: /(.*)/, assertion: 'Containers MUST have an ETag header'},
    199      { header: 'Vary', pat: /Accept/, assertion: 'Containers MUST have a Vary header with Accept in the value'},
    200      { header: 'Link', pat: /rel\=\"type\"|\/ns\/ldp#|Container/, assertion: 'Containers MUST advertise its type by including a link where the rel parameter value is type and the target IRI is the appropriate Container Type'},
    201      { header: 'Link', pat: /rel\=\"type\"|\/ns\/ldp#|Container/,
    202        assertion: 'Containers MUST advertise that it imposes Annotation protocol specific' +
    203                   ' constraints by including a link where the target IRI is' +
    204                   ' http://www.w3.org/TR/annotation-protocol/, and the rel parameter' +
    205                   ' value is the IRI http://www.w3.org/ns/ldp#constrainedBy'},
    206  ] );
    207 
    208 
    209  promise_test(function() {
    210      return request("HEAD", container_url).then(function(res) {
    211          assert_equals(res.status, 200, "HEAD request returned " + res.status);
    212          });
    213      }, "Containers MUST support HEAD method");
    214 
    215  promise_test(function() {
    216      return request("OPTIONS", container_url).then(function(res) {
    217          assert_equals(res.status, 200, "OPTIONS request returned " + res.status);
    218          });
    219      }, "Containers MUST support OPTIONS method");
    220 
    221  // Container representation tests
    222 
    223 
    224  makePromiseTests( theContainer, [
    225      { header: 'Content-Location', pat: /(.*)/, assertion: "Containers MUST include a Content-Location header with the IRI as its value" },
    226      { header: 'Content-Location', test: function(res) { if (res.xhr.getResponseHeader('content-location') === res.body.id ) { return true; } else { return false;} }, assertion: "Container's Content-Location and `id` MUST match" }
    227      ]);
    228 
    229  promise_test(function() {
    230    return theContainer.then(function(res) {
    231      var f = res.body.first;
    232      if (f !== undefined && f !== "") {
    233        request("GET", f).then(function(lres) {
    234          assert_true(('partOf' in lres.body) || ('id' in lres.body.partOf), "No partOf in response");
    235          });
    236      } else {
    237        assert_true(false, "no 'first' in response from Container");
    238      }
    239    });
    240  }, "Annotation Pages must have a link to the container they are part of, using the partOf property");
    241 
    242  promise_test(function() {
    243    return theContainer.then(function(res) {
    244      var l = res.body.last;
    245      request("GET", l).then(function(lres) {
    246        assert_true(('prev' in lres.body), "No link to the previous page in response");
    247        });
    248    });
    249  }, "Annotation Pages MUST have a link to the previous page in the sequence, using the prev property (if not the first page)");
    250 
    251  promise_test(function() {
    252    return theContainer.then(function(res) {
    253      var f = res.body.first;
    254      request("GET", f).then(function(lres) {
    255        assert_true(('next' in lres.body), "No link to the next page in response");
    256        });
    257    });
    258  }, "Annotation Pages MUST have a link to the next page in the sequence, using the next property (if not the last page)");
    259 
    260  // Annotation Tests
    261  var theRequest = request("GET", annotation_url);
    262 
    263  makePromiseTests( theRequest, [
    264      { header: 'Allow', pat: /GET/, assertion: "Annotations MUST support GET (check Allow on GET)" },
    265      { header: 'Allow', pat: /HEAD/, assertion: "Annotations MUST support HEAD (check Allow on GET)" },
    266      { header: 'Allow', pat: /OPTIONS/, assertion: "Annotations MUST support OPTIONS (check Allow on GET)" },
    267      { header: 'Content-Type', pat: MEDIA_TYPE_REGEX, assertion: 'Annotations MUST have a Content-Type header with the application/ld+json media type'},
    268      { header: 'Link', string: '<http://www.w3.org/ns/ldp#Resource>; rel="type"', assertion: 'Annotations MUST have a Link header entry where the target IRI is http://www.w3.org/ns/ldp#Resource and the rel parameter value is type'},
    269      { header: 'ETag', pat: /(.*)/, assertion: 'Annotations MUST have an ETag header'},
    270      { header: 'Vary', pat: /Accept/, assertion: 'Annotations MUST have a Vary header with Accept in the value'},
    271      ] );
    272 
    273  promise_test(function() {
    274    return request("HEAD", annotation_url).then(function(res) {
    275      assert_equals(res.status, 200, "HEAD request returned " + res.status);
    276      });
    277    }, "Annotations MUST support HEAD method");
    278 
    279  promise_test(function() {
    280    return request("OPTIONS", annotation_url).then(function(res) {
    281        assert_equals(res.status, 200, "OPTIONS request returned " + res.status);
    282      });
    283    }, "Annotations MUST support OPTIONS method");
    284 
    285 
    286  // creation and deletion tests
    287 
    288  var theAnnotation = {
    289    "@context": "http://www.w3.org/ns/anno.jsonld",
    290    "type": "Annotation",
    291    "body": {
    292      "type": "TextualBody",
    293      "value": "I like this page!"
    294    },
    295    "target": "http://www.example.com/index.html",
    296    "canonical": 'urn:uuid:' + token()
    297  };
    298 
    299  var theCreation = request("POST", container_url, [ [ 'Content-Type', MEDIA_TYPE ] ], theAnnotation);
    300 
    301  makePromiseTests( theCreation, [
    302      { test: function(res) { return ('id' in res.body); }, assertion: "Created Annotation MUST have an id property" },
    303      { test: function(res) { return (('id' in res.body) && (res.body.id.search(container_url) > -1));}, assertion: "Created Annotation MUST have an id that starts with the Container IRI" },
    304      { test: function(res) { return ( 'canonical' in res.body && res.body.canonical === theAnnotation.canonical ); }, assertion: "Created Annotation MUST preserve any canonical IRI" },
    305      { test: function(res) { return ( res.status === 201 ) ; }, assertion: "Annotation Server MUST respond with a 201 Created code if the creation is successful" },
    306      { header: "Location", test: function(res) { return res.body.id === res.xhr.getResponseHeader('location') ; } , assertion: "Location header SHOULD match the id of the new Annotation" },
    307      ]);
    308 
    309  promise_test(function() {
    310    return theCreation.then(function(res) {
    311      var newAnnotation = res.body ;
    312      newAnnotation.target = "http://other.example/";
    313      return request("PUT", res.body.id, [['Content-Type', MEDIA_TYPE]], newAnnotation)
    314        .then(function(lres) {
    315          assert_equals(lres.body.target, newAnnotation.target, "Annotation did not update");
    316        })
    317        .catch(function(err) {
    318          assert_true(false, "Update of annotation failed");
    319        });
    320    });
    321  }, "Annotation update must be done with the PUT method");
    322 
    323  promise_test(function() {
    324    return theCreation.then(function(res) {
    325      request("DELETE", res.body.id)
    326        .then(function(lres) {
    327          assert_equals(lres.status, 204, "DELETE of " + res.body.id + " did not return a 204 Status" );
    328        });
    329    });
    330  }, "Annotation deletion with DELETE method MUST return a 204 status" );
    331 
    332  // SHOULD tests
    333 
    334  test(function() {
    335    assert_equals(container_url.toLowerCase().substr(0,5), "https", "Server is not using HTTPS");
    336    }, "Annotation server SHOULD use HTTPS rather than HTTP");
    337 
    338  var thePrefRequest = request("GET", container_url,
    339      [['Prefer', 'return=representation;include="http://www.w3.org/ns/ldp#PreferMinimalContainer"']]);
    340 
    341  promise_test(function() {
    342    return thePrefRequest
    343      .then(function(res) {
    344        var f = res.body.first;
    345        request("GET", f).then(function(fres) {
    346          fres.body.items.forEach(function(item) {
    347            assert_true('@context' in item, "Annotation does not contain `@context`");
    348            });
    349          });
    350        });
    351      }, "SHOULD return the full annotation descriptions");
    352 
    353 
    354  makePromiseTests( thePrefRequest, [
    355      { test: function(res) { return ('total' in res.body); }, assertion: "SHOULD include the total property with the total number of annotations in the container" },
    356      { test: function(res) { return ('first' in res.body); }, assertion: "SHOULD have a link to the first page of its contents using `first`" },
    357      { test: function(res) { return ('last' in res.body); }, assertion: "SHOULD have a link to the last page of its contents using `last`" },
    358      { test: function(res) { return (!('items' in res.body)); }, assertion: "Response contains annotations via `items` when it SHOULD NOT"},
    359      { test: function(res) { return (!('ldp:contains' in res.body)); }, assertion: "Response contains annotations via `ldp:contains` when it SHOULD NOT" },
    360      { header: 'Vary', pat: /Prefer/, assertion: "SHOULD include Prefer in the Vary header" }
    361    ]);
    362 
    363  promise_test(function() {
    364    return thePrefRequest
    365      .then(function(res) {
    366        var h = res.xhr.getResponseHeader('Prefer');
    367        assert_equals(h, null, "Reponse contains the `Prefer` header when it SHOULD NOT");
    368        });
    369      }, 'SHOULD NOT [receive] the Prefer header when requesting the page');
    370 
    371 }
    372 
    373 // set up an event handler one the document is loaded that will run the tests once we
    374 // have a URI to run against
    375 on_event(document, "DOMContentLoaded", function() {
    376    var serverURI = document.getElementById("uri") ;
    377    var annotationURI = document.getElementById("annotation") ;
    378    var runButton = document.getElementById("endpoint-submit-button") ;
    379    on_event(runButton, "click", function() {
    380        // user clicked
    381        var URI = serverURI.value;
    382        var ANN = annotationURI.value;
    383        runButton.disabled = true;
    384 
    385        // okay, they clicked.  run the tests with that URI
    386        runTests(URI, ANN);
    387        done();
    388        });
    389    });
    390 </script>
    391 </head>
    392 <body>
    393 <p>The scripts associated with this test will exercise all of the MUST and SHOULD requirements
    394 for an Annotation Protocol server implementation.  In order to do so, the server must have
    395 its CORS settings configured such that your test machine can access the annotations and containers
    396 and such that it can get certain information from the headers.  In particular, the container and
    397 annotations within the container
    398 under test must permit access to the Allow, Content-Location, Content-Type, ETag, Link, Location, Prefer, and Vary headers.
    399 Correct CORS access can be achieved with headers like:</p>
    400 <pre>
    401 Access-Control-Allow-Headers: Content-Type, Prefer
    402 Access-Control-Allow-Methods: GET,HEAD,OPTIONS,DELETE,PUT
    403 Access-Control-Allow-Origin: *
    404 Access-Control-Expose-Headers: ETag, Allow, Vary, Link, Content-Type, Location, Content-Location, Prefer
    405 </pre>
    406 <p>Provide endpoint and annotation URIs and select "Go" to start testing.</p>
    407 <form name="endpoint">
    408  <p><label for="uri">Endpoint URI:</label> <input type="text" size="50" id="uri" name="uri"></p>
    409  <p><label for="uri">Annotation URI:</label> <input type="text" size="50" id="annotation" name="annotation"></p>
    410  <input type="button" id="endpoint-submit-button" value="Go">
    411 </form>
    412 </body>
    413 </html>