Add some functional CORS tests

If you've got selenium installed (and working), the whole thing can be
automated pretty well; run main.py, wait while some windows pop up (or
use xvfb-run to run things on a virtual display), then check out what
tests were run on which browsers and whether any of them failed. Exit
code is the number of failed tests.

Includes tests against:
- Account
- Containers, with various ACLs/CORS settings
- Objects
- /info
- SLOs
- DLOs
- Symlinks

Include a gate job that runs the tests in firefox.

Areas for future work:

- Install chromium and chromedriver in the gate; tests should
  automatically pick up on the fact that it's available
- Capture the web browser's console logs, too, so we can get
  more info when things go wrong

Change-Id: Ic1d3a062419f1133c6e2f00a598867d567358c9f
This commit is contained in:
Tim Burke 2018-01-11 16:24:24 -08:00
parent 5c3eb488f2
commit c5152ed4d3
14 changed files with 1425 additions and 4 deletions

View File

@ -309,6 +309,17 @@
vars:
bindep_profile: test py36
- job:
name: swift-func-cors
parent: swift-probetests-centos-7
description: |
Setup a SAIO dev environment and run Swift's CORS functional tests
timeout: 1200
pre-run:
- tools/playbooks/cors/install_selenium.yaml
run: tools/playbooks/cors/run.yaml
post-run: tools/playbooks/cors/post.yaml
- nodeset:
name: swift-five-nodes
nodes:
@ -515,7 +526,7 @@
- swift-tox-py27:
irrelevant-files: &unittest-irrelevant-files
- ^(api-ref|doc|releasenotes)/.*$
- ^test/(functional|probe)/.*$
- ^test/(cors|functional|probe)/.*$
- swift-tox-py36:
irrelevant-files: *unittest-irrelevant-files
- swift-tox-py37:
@ -529,7 +540,7 @@
- swift-tox-func-py27:
irrelevant-files: &functest-irrelevant-files
- ^(api-ref|doc|releasenotes)/.*$
- ^test/probe/.*$
- ^test/(cors|probe)/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG|.*\.rst)$
- swift-tox-func-encryption-py27:
irrelevant-files: *functest-irrelevant-files
@ -545,20 +556,27 @@
irrelevant-files: *functest-irrelevant-files
# Other tests
- swift-func-cors:
irrelevant-files:
- ^(api-ref|releasenotes)/.*$
# Keep doc/saio -- we use those sample configs in the saio playbooks
- ^doc/(requirements.txt|(manpages|s3api|source)/.*)$
- ^test/(unit|functional|probe)/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG)$
- swift-tox-func-s3api-ceph-s3tests-tempauth:
irrelevant-files:
- ^(api-ref|releasenotes)/.*$
# Keep doc/saio -- we use those sample configs in the saio playbooks
# Also keep doc/s3api -- it holds known failures for these tests
- ^doc/(requirements.txt|(manpages|source)/.*)$
- ^test/(unit|probe)/.*$
- ^test/(cors|unit|probe)/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG|.*\.rst)$
- swift-probetests-centos-7:
irrelevant-files: &probetest-irrelevant-files
- ^(api-ref|releasenotes)/.*$
# Keep doc/saio -- we use those sample configs in the saio playbooks
- ^doc/(requirements.txt|(manpages|s3api|source)/.*)$
- ^test/(unit|functional)/.*$
- ^test/(cors|unit|functional)/.*$
- ^(.gitreview|.mailmap|AUTHORS|CHANGELOG|.*\.rst)$
- swift-probetests-centos-8:
irrelevant-files: *probetest-irrelevant-files
@ -606,6 +624,7 @@
- swift-tox-func-py37
- swift-tox-func-encryption-py37
- swift-tox-func-ec-py37
- swift-func-cors
- swift-probetests-centos-7:
irrelevant-files: *probetest-irrelevant-files
- swift-probetests-centos-8:

97
test/cors/README.rst Normal file
View File

@ -0,0 +1,97 @@
CORS Functional Tests
=====================
`Cross Origin Resource Sharing <https://www.w3.org/TR/cors/>`__ is a bit
of a complicated beast. It focuses on the interactions between
* a **user-agent** (typically a web browser),
* a "**source origin**" server (whose code the user-agent is running), and
* some **other server** (for our purposes, usually Swift).
Where it gets hairy is that there may be varying degrees of trust between
these different actors.
Fortunately, Swift `allows per-container configuration
<https://docs.openstack.org/swift/latest/cors.html>`__ of many CORS options.
However, our normal functional tests only exercise bits and pieces of CORS,
without telling a complete story or performing a true end-to-end test. *These*
tests aim to remedy that.
The tests consist of three parts:
* setup
Create several test containers with well-known names, set appropriate
ACLs and CORS metadata, and upload some test objects.
* serve
Serve a static website on localhost which, on load, will make several
CORS requests and verify expected behavior.
* run
Use Selenium to load the website, wait for and scrape the results, and
output them in `TAP format <http://testanything.org/tap-specification.html>`__.
Alternatively, open the page in your local browser and manually inspect whether
tests passed or failed.
All of this is orchestrated through ``main.py``. It uses the standard ``OS_*``
environment variables to determine how to connect to Swift:
* ``OS_AUTH_URL`` (or ``ST_AUTH``)
* ``OS_USERNAME`` (or ``ST_USER``)
* ``OS_PASSWORD`` (or ``ST_KEY``)
* ``OS_STORAGE_URL`` (optional)
..
TODO: verify that this works with Keystone
Running Tests Manually
----------------------
To inspect the test results in your local browser, run::
$ ./test/cors/main.py --no-run
This will create some test containers and object in Swift, start a simple
static site, and emit a URL to visit to run the tests, like::
Serving test at http://localhost:8000/#OS_AUTH_URL=http://saio/auth/v1.0&OS_USERNAME=test:tester&OS_PASSWORD=testing&OS_STORAGE_URL=http://saio/v1/AUTH_test
.. note::
You can use ``--hostname`` and ``--port`` to adjust the origin used.
Open the link. Toward the top of the page will be a status line; it will cycle
through the following states:
* Loading
* Starting jobs
* Waiting for jobs to finish
* Complete
When complete, it will also include a summary of the number of tests run as
well as pass/fail/skip counts. Below the status line will be a table of
individual tests with status, description, and additional information.
You can also run a single test by adding a ``&test=<name>`` query parameter.
For example::
http://localhost:8000/#OS_AUTH_URL=http://saio/auth/v1.0&OS_USERNAME=test:tester&OS_PASSWORD=testing&OS_STORAGE_URL=http://saio/v1/AUTH_test&test=object%20-%20GET
will just run the test named ``object - GET``.
To stop the server, press ``^C``.
Running Tests with Selenium
---------------------------
`Selenium <https://www.selenium.dev/>`__ may be used to automate visiting the
static site, waiting for tests to run, and gathering results. See the
`installation instructions <https://selenium-python.readthedocs.io/installation.html>`__
for the Python bindings for more information about setting this up.
.. note::
On Linux, you may want to use ``xvfb-run`` to have browsers use a virtual
display.
When using selenium, the test runner will try to run tests in Firefox, Chrome,
Safari, Edge, and IE if available; if a browser seems to not be available, its
tests will be skipped.

251
test/cors/harness.js Normal file
View File

@ -0,0 +1,251 @@
/* global PARAMS, XMLHttpRequest */
const STORAGE_URL = PARAMS.OS_STORAGE_URL || 'http://localhost:8080/v1/AUTH_test'
function makeUrl (path) {
if (path.startsWith('http://') || path.startsWith('https://')) {
return new URL(path)
}
if (!path.startsWith('/')) {
return new URL(STORAGE_URL + '/' + path)
}
return new URL(STORAGE_URL.substr(0, STORAGE_URL.indexOf('/', 3 + STORAGE_URL.indexOf('://'))) + path)
}
export function MakeRequest (method, path, headers, body, params) {
var url = makeUrl(path)
params = params || {}
// give each request a unique query string to avoid ever fetching from cache
params['cors-test-time'] = Date.now().toString()
params['cors-test-random'] = Math.random().toString()
for (var key in params) {
url.searchParams.append(key, params[key])
}
return new Promise((resolve, reject) => {
const req = new XMLHttpRequest()
req.addEventListener('readystatechange', function () {
if (this.readyState === 4) {
resolve(this)
}
})
req.open(method, url.toString())
if (headers) {
for (const name of Object.keys(headers)) {
req.setRequestHeader(name, headers[name])
}
}
req.send(body)
})
}
export function HasStatus (expectedStatus, expectedMessage) {
return function (resp) {
if (resp.status !== expectedStatus) {
throw new Error('Expected status ' + expectedStatus + ', got ' + resp.status)
}
if (resp.statusText !== expectedMessage) {
throw new Error('Expected status text ' + expectedMessage + ', got ' + resp.statusText)
}
return resp
}
}
export function HasHeaders (headers) {
if (headers instanceof Array) {
return function (resp) {
const missing = headers.filter((h) => !resp.getResponseHeader(h))
if (missing.length) {
throw new Error('Missing expected headers ' + JSON.stringify(missing) + ' in response: ' + resp.getAllResponseHeaders())
}
return resp
}
} else {
return function (resp) {
const names = Object.keys(headers)
const missing = names.filter((h) => !resp.getResponseHeader(h))
if (missing.length) {
throw new Error('Missing expected headers ' + JSON.stringify(missing) + ' in response: ' + resp.getAllResponseHeaders())
}
for (const name of names) {
const value = resp.getResponseHeader(name)
if (name === 'Etag') {
// special case for Etag which may or may not be quoted
if ((value !== headers[name]) && (value !== "\"" + headers[name] + "\"")) {
throw new Error('Expected header ' + name + ' to have value ' + headers[name] + ', got ' + value)
}
}
else if (value !== headers[name]) {
throw new Error('Expected header ' + name + ' to have value ' + headers[name] + ', got ' + value)
}
}
return resp
}
}
}
export function HasCommonResponseHeaders (resp) {
// These appear in most *all* responses, but have unpredictable values
HasHeaders([
'Last-Modified',
'X-Openstack-Request-Id',
'X-Timestamp',
'X-Trans-Id',
'Content-Type'
])(resp)
// Save that trans-id and request-id are the same thing
if (resp.getResponseHeader('X-Trans-Id') !== resp.getResponseHeader('X-Openstack-Request-Id')) {
throw new Error('Expected X-Trans-Id and X-Openstack-Request-Id to match; got ' + resp.getAllResponseHeaders())
}
// These appear in most responses, but *aren't* (currently) exposed via CORS
DoesNotHaveHeaders([
'Accept-Ranges',
'Access-Control-Allow-Origin',
'Access-Control-Expose-Headers',
'Date',
// Hmmm....
'Content-Range',
'X-Account-Bytes-Used',
'X-Account-Container-Count',
'X-Account-Object-Count',
'X-Container-Bytes-Used',
'X-Container-Object-Count'
])(resp)
return resp
}
export function DoesNotHaveHeaders (headers) {
return function (resp) {
const found = headers.filter((h) => resp.getResponseHeader(h))
if (found.length) {
throw new Error('Found unexpected headers ' + found + ' in response: ' + resp.getAllResponseHeaders())
}
return resp
}
}
export function HasNoBody (resp) {
if (resp.responseText !== '') {
throw new Error('Expected no response body; got ' + resp.responseText)
}
return resp
}
export function BodyHasLength (expectedLength) {
return (resp) => {
if (resp.responseText.length !== expectedLength) {
throw new Error('Expected body to have length ' + expectedLength + ', got ' + resp.responseText.length)
}
return resp
}
}
export function CorsBlocked (resp) {
// Yeah, there's *nothing* useful here -- gotta look at the browser's console if you want to see what happened
HasStatus(0, '')(resp)
const allHeaders = resp.getAllResponseHeaders()
if (allHeaders !== '') {
throw new Error('Expected no headers; got ' + allHeaders)
}
HasNoBody(resp)
return resp
}
function _denial (status, text) {
function Denial (resp) {
HasStatus(status, text)(resp)
const prefix = '<html><h1>' + text + '</h1>'
if (!resp.responseText.startsWith(prefix)) {
throw new Error('Expected body to start with ' + JSON.stringify(prefix) + '; got ' + JSON.stringify(resp.responseText))
}
HasHeaders({ 'Content-Type': 'text/html; charset=UTF-8' })(resp)
HasHeaders([
'X-Openstack-Request-Id',
'X-Trans-Id',
'Content-Type'
])(resp)
if (resp.getResponseHeader('X-Trans-Id') !== resp.getResponseHeader('X-Openstack-Request-Id')) {
throw new Error('Expected X-Trans-Id and X-Openstack-Request-Id to match; got ' + resp.getAllResponseHeaders())
}
DoesNotHaveHeaders([
'X-Account-Bytes-Used',
'X-Account-Container-Count',
'X-Account-Object-Count',
'X-Container-Bytes-Used',
'X-Container-Object-Count',
'Etag',
'X-Object-Meta-Mtime',
'Last-Modified',
'X-Timestamp',
'Accept-Ranges',
'Access-Control-Allow-Origin',
'Access-Control-Expose-Headers',
'Date',
// Hmmm....
'Content-Range'
])(resp)
return resp
}
return Denial
}
export const Unauthorized = _denial(401, 'Unauthorized')
export const NotFound = _denial(404, 'Not Found')
const $new = document.createElement.bind(document)
export function Skip (msg) {
this.message = msg
}
Skip.prototype = new Error()
const testPromises = []
export function runTests (prefix, tests) {
for (let i = 0; i < tests.length; ++i) {
const [name, test] = tests[i]
const fullName = prefix + ' - ' + name
if ('test' in PARAMS && PARAMS['test'] !== fullName) {
continue
}
const row = document.getElementById('results').appendChild($new('tr'))
row.appendChild($new('td')).textContent = 'Queued'
row.appendChild($new('td')).textContent = fullName
row.appendChild($new('td'))
testPromises.push(
test().then((resp) => {
row.childNodes[0].className = 'pass'
row.childNodes[0].textContent = 'PASS'
}).catch((reason) => {
if (reason instanceof Skip) {
row.childNodes[0].className = 'skip'
row.childNodes[0].textContent = 'SKIP'
row.childNodes[2].textContent = reason.message
} else {
row.childNodes[0].className = 'fail'
row.childNodes[0].textContent = 'FAIL'
row.childNodes[2].textContent = reason.message || reason
if (reason.stack) {
row.childNodes[2].textContent += '\n' + reason.stack
}
throw reason
}
})
)
}
}
window.addEventListener('load', function () {
document.getElementById('status').textContent = 'Waiting for all ' + testPromises.length + ' tests to finish...'
// Poor-man's version of something approximating
// Promise.allSettled(testPromises).then((results) => {
// for Firefox < 71, Chrome < 76, etc.
Promise.all(testPromises.map(x => x.then((x) => x, (x) => x))).then(() => {
const resultTable = document.getElementById('results')
document.getElementById('status').textContent = (
'Complete.' +
' TESTS: ' + resultTable.childNodes.length +
' PASS: ' + resultTable.querySelectorAll('.pass').length +
' FAIL: ' + resultTable.querySelectorAll('.fail').length +
' SKIP: ' + resultTable.querySelectorAll('.skip').length
)
})
})

42
test/cors/index.html Normal file
View File

@ -0,0 +1,42 @@
<html>
<head>
<meta charset="utf-8" />
<title>CORS Tests</title>
<style type="text/css">
tr:nth-child(2n) { background: lightgrey; }
.pass { background: green; }
.fail { background: red; }
.skip { background: orange; }
td:nth-child(1) {
padding: .1em;
width: 8em;
text-align: center;
}
td:nth-child(2) {
width: 50%;
}
</style>
<script type="text/javascript">
const PARAMS = !window.location.hash ? {} : window.location.hash.substr(1)
.split('&')
.map(v => v.split('='))
.reduce( (acc, [key, val]) => ({ ...acc, [unescape(key)]: unescape(val) }), {})
console.log(PARAMS)
</script>
<script type="module" src="test-info.js"></script>
<script type="module" src="test-account.js"></script>
<script type="module" src="test-container.js"></script>
<script type="module" src="test-object.js"></script>
<script type="module" src="test-large-objects.js"></script>
<script type="module" src="test-symlink.js"></script>
</head>
<body>
<h2>CORS Tests</h2>
<div id="status">Loading...</div>
<table>
<thead><th>Result</th><th>Name</th><th>Details</th></thead>
<tbody id="results"></tbody>
</table>
<pre id="dumper"></pre>
</body>
</html>

317
test/cors/main.py Executable file
View File

@ -0,0 +1,317 @@
#!/usr/bin/env python
# Copyright (c) 2020 SwiftStack, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import json
import os
import os.path
import sys
import threading
import time
from six.moves import urllib
from six.moves import socketserver
from six.moves import SimpleHTTPServer
try:
import selenium.webdriver
except ImportError:
selenium = None
import swiftclient.client
DEFAULT_ENV = {
'OS_AUTH_URL': os.environ.get('ST_AUTH',
'http://localhost:8080/auth/v1.0'),
'OS_USERNAME': os.environ.get('ST_USER', 'test:tester'),
'OS_PASSWORD': os.environ.get('ST_KEY', 'testing'),
'OS_STORAGE_URL': None,
}
ENV = {key: os.environ.get(key, default)
for key, default in DEFAULT_ENV.items()}
TEST_TIMEOUT = 120.0 # seconds
STEPS = 500
# Hack up stdlib so SimpleHTTPRequestHandler works well on py2, too
this_dir = os.path.realpath(os.path.dirname(__file__))
os.getcwd = lambda: this_dir
class CORSSiteHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def log_message(self, fmt, *args):
pass # quiet, you!
class CORSSiteServer(socketserver.TCPServer):
allow_reuse_address = True
class CORSSite(threading.Thread):
def __init__(self, bind_port=8000):
super(CORSSite, self).__init__()
self.server = None
self.bind_port = bind_port
def run(self):
self.server = CORSSiteServer(
('0.0.0.0', self.bind_port),
CORSSiteHandler)
self.server.serve_forever()
def terminate(self):
if self.server is not None:
self.server.shutdown()
self.join()
class Zeroes(object):
BUF = b'\x00' * 64 * 1024
def __init__(self, size=0):
self.pos = 0
self.size = size
def __iter__(self):
while self.pos < self.size:
chunk = self.BUF[:self.size - self.pos]
self.pos += len(chunk)
yield chunk
def __len__(self):
return self.size
def setup(args):
conn = swiftclient.client.Connection(
ENV['OS_AUTH_URL'],
ENV['OS_USERNAME'],
ENV['OS_PASSWORD'],
timeout=5)
cluster_info = conn.get_capabilities()
conn.put_container('private', {
'X-Container-Read': '',
'X-Container-Meta-Access-Control-Allow-Origin': '',
})
conn.put_container('referrer-allowed', {
'X-Container-Read': '.r:%s' % args.hostname,
'X-Container-Meta-Access-Control-Allow-Origin': (
'http://%s:%d' % (args.hostname, args.port)),
})
conn.put_container('other-referrer-allowed', {
'X-Container-Read': '.r:other-host',
'X-Container-Meta-Access-Control-Allow-Origin': 'http://other-host',
})
conn.put_container('public-with-cors', {
'X-Container-Read': '.r:*,.rlistings',
'X-Container-Meta-Access-Control-Allow-Origin': '*',
})
conn.put_container('private-with-cors', {
'X-Container-Read': '',
'X-Container-Meta-Access-Control-Allow-Origin': '*',
})
conn.put_container('public-no-cors', {
'X-Container-Read': '.r:*,.rlistings',
'X-Container-Meta-Access-Control-Allow-Origin': '',
})
conn.put_container('public-segments', {
'X-Container-Read': '.r:*',
'X-Container-Meta-Access-Control-Allow-Origin': '',
})
for container in ('private', 'referrer-allowed', 'other-referrer-allowed',
'public-with-cors', 'private-with-cors',
'public-no-cors'):
conn.put_object(container, 'obj', Zeroes(1024), headers={
'X-Object-Meta-Mtime': str(time.time())})
for n in range(10):
segment_etag = conn.put_object(
'public-segments', 'seg%02d' % n, Zeroes(1024 * 1024),
headers={'Content-Type': 'application/swiftclient-segment'})
conn.put_object(
'public-with-cors', 'dlo/seg%02d' % n, Zeroes(1024 * 1024),
headers={'Content-Type': 'application/swiftclient-segment'})
conn.put_object('public-with-cors', 'dlo-with-unlistable-segments', b'',
headers={'X-Object-Manifest': 'public-segments/seg'})
conn.put_object('public-with-cors', 'dlo', b'',
headers={'X-Object-Manifest': 'public-with-cors/dlo/seg'})
if 'slo' in cluster_info:
conn.put_object('public-with-cors', 'slo', json.dumps([
{'path': 'public-segments/seg%02d' % n, 'etag': segment_etag}
for n in range(10)]), query_string='multipart-manifest=put')
if 'symlink' in cluster_info:
for tgt in ('private', 'public-with-cors', 'public-no-cors'):
conn.put_object('public-with-cors', 'symlink-to-' + tgt, b'',
headers={'X-Symlink-Target': tgt + '/obj'})
def get_results_table(browser):
result_table = browser.find_element_by_id('results')
for row in result_table.find_elements_by_xpath('./tr'):
cells = row.find_elements_by_xpath('td')
yield (
cells[0].text,
browser.name + ': ' + cells[1].text,
cells[2].text)
def run(args, url):
results = []
browsers = list(ALL_BROWSERS) if 'all' in args.browsers else args.browsers
ran_one = False
for browser_name in browsers:
driver = getattr(selenium.webdriver, browser_name.title())
try:
browser = driver()
except Exception as e:
results.append(('SKIP', browser_name, str(e).strip()))
continue
ran_one = True
try:
browser.get(url)
start = time.time()
for _ in range(STEPS):
status = browser.find_element_by_id('status').text
if status.startswith('Complete'):
results.extend(get_results_table(browser))
break
time.sleep(TEST_TIMEOUT / STEPS)
else:
try:
results.extend(get_results_table(browser))
except Exception:
pass # worth a shot
# that took a sec; give it *one last chance* to succeed
status = browser.find_element_by_id('status').text
if not status.startswith('Complete'):
results.append((
'ERROR', browser_name, 'Timed out (%s)' % status))
continue
sys.stderr.write('Tested %s in %.1fs\n' % (
browser_name, time.time() - start))
except Exception as e:
results.append(('ERROR', browser_name, str(e).strip()))
finally:
browser.close()
if args.output is not None:
fp = open(args.output, 'w')
else:
fp = sys.stdout
fp.write('1..%d\n' % len(results))
rc = 0
if not ran_one:
rc += 1 # make sure "no tests ran" translates to "failed"
for test, (status, name, details) in enumerate(results, start=1):
if status == 'PASS':
fp.write('ok %d - %s\n' % (test, name))
elif status == 'SKIP':
fp.write('ok %d - %s # skip %s\n' % (test, name, details))
else:
fp.write('not ok %d - %s\n' % (test, name))
fp.write(' %s%s\n' % (status, ':' if details else ''))
if details:
fp.write(''.join(
' ' + line + '\n'
for line in details.split('\n')))
rc += 1
if fp is not sys.stdout:
fp.close()
return rc
ALL_BROWSERS = [
'firefox',
'chrome',
'safari',
'edge',
'ie',
]
if __name__ == '__main__':
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description='Set up and run CORS functional tests',
epilog='''The tests consist of three parts:
setup - Create several test containers with well-known names, set appropriate
ACLs and CORS metadata, and upload some test objects.
serve - Serve a static website on localhost which, on load, will make several
CORS requests and verify expected behavior.
run - Use Selenium to load the website, wait for and scrape the results,
and output them in TAP format.
By default, perform all three parts. You can skip some or all of the parts
with the --no-setup, --no-serve, and --no-run options.
''')
parser.add_argument('-P', '--port', type=int, default=8000)
parser.add_argument('-H', '--hostname', default='localhost')
parser.add_argument('--no-setup', action='store_true')
parser.add_argument('--no-serve', action='store_true')
parser.add_argument('--no-run', action='store_true')
parser.add_argument('-o', '--output')
parser.add_argument('browsers', nargs='*',
default='all',
choices=['all'] + ALL_BROWSERS)
args = parser.parse_args()
if not args.no_setup:
setup(args)
if args.no_serve:
site = None
else:
site = CORSSite(args.port)
should_run = not args.no_run
if should_run and not selenium:
print('Selenium not available; cannot run tests automatically')
should_run = False
if ENV['OS_STORAGE_URL'] is None:
ENV['OS_STORAGE_URL'] = swiftclient.client.get_auth(
ENV['OS_AUTH_URL'],
ENV['OS_USERNAME'],
ENV['OS_PASSWORD'],
timeout=1)[0]
url = 'http://%s:%d/#%s' % (args.hostname, args.port, '&'.join(
'%s=%s' % (urllib.parse.quote(key), urllib.parse.quote(val))
for key, val in ENV.items()))
rc = 0
if should_run:
if site:
site.start()
try:
rc = run(args, url)
finally:
if site:
site.terminate()
else:
if site:
print('Serving test at %s' % url)
try:
site.run()
except KeyboardInterrupt:
pass
exit(rc)

16
test/cors/test-account.js Normal file
View File

@ -0,0 +1,16 @@
import { runTests, MakeRequest, CorsBlocked } from './harness.js'
runTests('account', [
['GET', () => MakeRequest('GET', '')
// 200, but missing Access-Control-Allow-Origin
.then(CorsBlocked)],
['HEAD', () => MakeRequest('HEAD', '')
// 200, but missing Access-Control-Allow-Origin
.then(CorsBlocked)],
['POST', () => MakeRequest('POST', '')
// 200, but missing Access-Control-Allow-Origin
.then(CorsBlocked)],
['POST with meta', () => MakeRequest('POST', '', { 'X-Account-Meta-Never-Makes-It': 'preflight failed' })
// preflight 200s, but it's missing Access-Control-Allow-Origin
.then(CorsBlocked)]
])

148
test/cors/test-container.js Normal file
View File

@ -0,0 +1,148 @@
import {
runTests,
MakeRequest,
HasStatus,
HasHeaders,
HasCommonResponseHeaders,
HasNoBody
} from './harness.js'
function CheckJsonListing (resp) {
HasHeaders({ 'Content-Type': 'application/json; charset=utf-8' })(resp)
const listing = JSON.parse(resp.responseText)
for (const item of listing) {
if ('subdir' in item) {
if (Object.keys(item).length !== 1) {
throw new Error('Expected subdir to be the only key, got ' + JSON.stringify(item))
}
continue
}
const missing = ['name', 'bytes', 'content_type', 'hash', 'last_modified'].filter((key) => !(key in item))
if (missing.length) {
throw new Error('Listing item is missing expected keys ' + JSON.stringify(missing) + '; got ' + JSON.stringify(item))
}
}
return listing
}
function HasStatus200Or204 (resp) {
if (resp.status === 200) {
// NB: some browsers (like chrome) may serve HEADs from cached GETs, leading to the 200
HasStatus(200, 'OK')(resp)
} else {
HasStatus(204, 'No Content')(resp)
}
return resp
}
const expectedListing = [
'dlo',
'dlo-with-unlistable-segments',
'dlo/seg00',
'dlo/seg01',
'dlo/seg02',
'dlo/seg03',
'dlo/seg04',
'dlo/seg05',
'dlo/seg06',
'dlo/seg07',
'dlo/seg08',
'dlo/seg09',
'obj',
'slo',
'symlink-to-private',
'symlink-to-public-no-cors',
'symlink-to-public-with-cors'
]
const expectedWithDelimiter = [
'dlo',
'dlo-with-unlistable-segments',
'dlo/',
'obj',
'slo',
'symlink-to-private',
'symlink-to-public-no-cors',
'symlink-to-public-with-cors'
]
runTests('container', [
['GET format=txt',
() => MakeRequest('GET', 'public-with-cors', {}, '', {'format': 'txt'})
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }))
.then((resp) => {
const names = resp.responseText.split('\n')
if (!(names.length === expectedListing.length + 1 && names.every((name, i) => name === (i === expectedListing.length ? '' : expectedListing[i])))) {
throw new Error('Expected listing to have items ' + JSON.stringify(expectedListing) + '; got ' + JSON.stringify(names))
}
})],
['GET format=json',
() => MakeRequest('GET', 'public-with-cors', {}, '', {'format': 'json'})
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(CheckJsonListing)
.then((listing) => {
const names = listing.map((item) => 'subdir' in item ? item.subdir : item.name)
if (!(names.length === expectedListing.length && names.every((name, i) => expectedListing[i] === name))) {
throw new Error('Expected listing to have items ' + JSON.stringify(expectedListing) + '; got ' + JSON.stringify(names))
}
})],
['GET format=json&delimiter=/',
() => MakeRequest('GET', 'public-with-cors', {}, '', {'format': 'json', 'delimiter': '/'})
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(CheckJsonListing)
.then((listing) => {
const names = listing.map((item) => 'subdir' in item ? item.subdir : item.name)
if (!(names.length === expectedWithDelimiter.length && names.every((name, i) => expectedWithDelimiter[i] === name))) {
throw new Error('Expected listing to have items ' + JSON.stringify(expectedWithDelimiter) + '; got ' + JSON.stringify(names))
}
})],
['GET format=xml',
() => MakeRequest('GET', 'public-with-cors', {}, '', {'format': 'xml'})
.then(HasStatus(200, 'OK'))
.then(HasHeaders({ 'Content-Type': 'application/xml; charset=utf-8' }))
.then((resp) => {
const prefix = '<?xml version="1.0" encoding="UTF-8"?>\n<container name="public-with-cors">'
if (resp.responseText.substr(0, prefix.length) !== prefix) {
throw new Error('Expected response to start with ' + JSON.stringify(prefix) + '; got ' + resp.responseText)
}
})],
['GET Accept: json',
() => MakeRequest('GET', 'public-with-cors', { Accept: 'application/json' })
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(CheckJsonListing)
.then((listing) => {
if (listing.length !== 17) {
throw new Error('Expected exactly 17 items in listing; got ' + listing.length)
}
})],
['GET Accept: xml',
// NB: flakey on Safari -- sometimes it serves JSON from cache, *even with* a Vary: Accept header
() => MakeRequest('GET', 'public-with-cors', { Accept: 'application/xml' })
.then(HasStatus(200, 'OK'))
.then(HasHeaders({ 'Content-Type': 'application/xml; charset=utf-8' }))
.then((resp) => {
const prefix = '<?xml version="1.0" encoding="UTF-8"?>\n<container name="public-with-cors">'
if (resp.responseText.substr(0, prefix.length) !== prefix) {
throw new Error('Expected response to start with ' + JSON.stringify(prefix) + '; got ' + resp.responseText)
}
})],
['HEAD format=txt',
() => MakeRequest('HEAD', 'public-with-cors', {}, '', {'format': 'txt'})
.then(HasStatus200Or204)
.then(HasHeaders({ 'Content-Type': 'text/plain; charset=utf-8' }))
.then(HasNoBody)],
['HEAD format=json',
() => MakeRequest('HEAD', 'public-with-cors', {}, '', {'format': 'json'})
.then(HasStatus200Or204)
.then(HasHeaders({ 'Content-Type': 'application/json; charset=utf-8' }))
.then(HasNoBody)],
['HEAD format=xml',
() => MakeRequest('HEAD', 'public-with-cors', {}, '', {'format': 'xml'})
.then(HasStatus200Or204)
.then(HasHeaders({ 'Content-Type': 'application/xml; charset=utf-8' }))
.then(HasNoBody)]
])

60
test/cors/test-info.js Normal file
View File

@ -0,0 +1,60 @@
import {
runTests,
MakeRequest,
HasStatus,
HasHeaders,
DoesNotHaveHeaders,
HasNoBody,
CorsBlocked
} from './harness.js'
function CheckInfoHeaders (resp) {
return Promise.resolve(resp)
.then(HasHeaders({ 'Content-Type': 'application/json; charset=UTF-8' }))
.then(HasHeaders(['X-Trans-Id']))
.then(DoesNotHaveHeaders([
'X-Openstack-Request-Id', // TODO: this is blocked by CORS but almost certainly shouldn't
'X-Timestamp',
'Accept-Ranges',
'Access-Control-Allow-Origin',
'Access-Control-Expose-Headers',
'Date',
'Content-Range'
]))
}
function CheckInfoBody (resp) {
const clusterInfo = JSON.parse(resp.responseText)
if (!('swift' in clusterInfo)) {
throw new Error('Expected to find "swift" in /info response; ' +
'got ' + JSON.stringify(clusterInfo))
}
if (!('version' in clusterInfo.swift)) {
throw new Error('Expected to find "swift.version" in /info response; ' +
'got ' + JSON.stringify(clusterInfo.swift))
}
console.log('Tested against Swift version ' + clusterInfo.swift.version)
return clusterInfo
}
export const GetClusterInfo = MakeRequest('GET', '/info')
.then(HasStatus(200, 'OK'))
.then(CheckInfoHeaders)
.then(CheckInfoBody)
// TODO: /info should probably get an automatic access-control-allow-origin: *
runTests('cluster info', [
['GET', () => GetClusterInfo],
['GET with header', () => MakeRequest('GET', '/info', { 'X-Trans-Id-Extra': 'my-tracker' })
// 200, but missing Access-Control-Allow-Origin
.then(CorsBlocked)],
['HEAD', () => MakeRequest('HEAD', '/info')
.then(HasStatus(200, 'OK'))
.then(CheckInfoHeaders)
.then(HasNoBody)],
['OPTIONS', () => MakeRequest('OPTIONS', '/info')
// 200, but missing Access-Control-Allow-Origin
.then(CorsBlocked)],
['POST', () => MakeRequest('POST', '/info')
// 405, but missing Access-Control-Allow-Origin
.then(CorsBlocked)]
])

View File

@ -0,0 +1,93 @@
import {
runTests,
MakeRequest,
HasStatus,
HasHeaders,
HasCommonResponseHeaders,
DoesNotHaveHeaders,
HasNoBody,
CorsBlocked,
Skip
} from './harness.js'
import { GetClusterInfo } from './test-info.js'
function MakeSloRequest () {
return GetClusterInfo.then((clusterInfo) => {
if (!('slo' in clusterInfo)) {
throw new Skip('SLO is not enabled')
}
return MakeRequest(...arguments)
})
}
runTests('large object', [
['GET DLO',
() => MakeRequest('GET', 'public-with-cors/dlo')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
ETag: '"8d431e7531abb83a6cf67e56d91c6f74"'
}))
.then(DoesNotHaveHeaders(['X-Object-Manifest'])) // TODO: should maybe be exposed
.then((resp) => {
if (resp.responseText.length !== 10485760) {
throw new Error('Expected body to have length 10485760, got ' + resp.responseText.length)
}
})],
['GET DLO with unlistable segments',
() => MakeRequest('GET', 'public-with-cors/dlo-with-unlistable-segments')
.then(CorsBlocked)], // TODO: should probably be Unauthorized
['GET SLO',
() => MakeSloRequest('GET', 'public-with-cors/slo')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
ETag: '"8d431e7531abb83a6cf67e56d91c6f74"'
}))
.then(DoesNotHaveHeaders(['X-Static-Large-Object'])) // TODO: should maybe be exposed
.then((resp) => {
if (resp.responseText.length !== 10485760) {
throw new Error('Expected body to have length 10485760, got ' + resp.responseText.length)
}
})],
['HEAD SLO',
() => MakeSloRequest('HEAD', 'public-with-cors/slo')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
ETag: '"8d431e7531abb83a6cf67e56d91c6f74"'
}))
.then(DoesNotHaveHeaders(['X-Static-Large-Object'])) // TODO: should maybe be exposed
.then(HasNoBody)],
['GET SLO Range',
() => MakeSloRequest('GET', 'public-with-cors/slo', { Range: 'bytes=100-199' })
.then(HasStatus(206, 'Partial Content'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
ETag: '"8d431e7531abb83a6cf67e56d91c6f74"'
}))
.then(DoesNotHaveHeaders(['X-Static-Large-Object'])) // TODO: should maybe be exposed
.then((resp) => {
if (resp.responseText.length !== 100) {
throw new Error('Expected body to have length 100, got ' + resp.responseText.length)
}
})],
['GET SLO Suffix Range',
() => MakeSloRequest('GET', 'public-with-cors/slo', { Range: 'bytes=-100' })
.then(HasStatus(206, 'Partial Content'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
ETag: '"8d431e7531abb83a6cf67e56d91c6f74"'
}))
.then(DoesNotHaveHeaders(['X-Static-Large-Object'])) // TODO: should maybe be exposed
.then((resp) => {
if (resp.responseText.length !== 100) {
throw new Error('Expected body to have length 100, got ' + resp.responseText.length)
}
})]
])

169
test/cors/test-object.js Normal file
View File

@ -0,0 +1,169 @@
import {
runTests,
MakeRequest,
HasStatus,
HasHeaders,
HasCommonResponseHeaders,
HasNoBody,
BodyHasLength,
CorsBlocked,
NotFound,
Unauthorized
} from './harness.js'
runTests('object', [
['GET',
() => MakeRequest('GET', 'public-with-cors/obj')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(1024))],
['HEAD',
() => MakeRequest('HEAD', 'public-with-cors/obj')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({ 'Content-Type': 'application/octet-stream' }))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(HasNoBody)],
['GET Range',
() => MakeRequest('GET', 'public-with-cors/obj', { Range: 'bytes=100-199' })
.then(HasStatus(206, 'Partial Content'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(100))],
['GET If-Match matching',
() => MakeRequest('GET', 'public-with-cors/obj', { 'If-Match': '0f343b0931126a20f133d67c2b018a3b' })
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(1024))],
['GET If-Match not matching',
() => MakeRequest('GET', 'public-with-cors/obj', { 'If-Match': 'something-else' })
.then(HasStatus(412, 'Precondition Failed'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'text/html; charset=UTF-8',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(HasNoBody)],
['GET If-None-Match matching',
() => MakeRequest('GET', 'public-with-cors/obj', { 'If-None-Match': '0f343b0931126a20f133d67c2b018a3b' })
.then(HasStatus(304, 'Not Modified'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
// TODO: Content-Type can vary depending on storage policy type...
// 'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['Content-Type', 'X-Object-Meta-Mtime']))
.then(HasNoBody)],
['GET If-None-Match not matching',
() => MakeRequest('GET', 'public-with-cors/obj', { 'If-None-Match': 'something-else' })
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(1024))],
['GET not found',
() => MakeRequest('GET', 'public-with-cors/should-404')
.then(NotFound)],
['POST',
() => MakeRequest('POST', 'public-with-cors/obj')
// No good way to make a container publicly-writable
.then(Unauthorized)],
['POST with meta',
() => MakeRequest('POST', 'public-with-cors/obj', { 'X-Object-Meta-Foo': 'bar' })
// Still no good way to make a container publicly-writable, but notably,
// *the POST goes through* and this isn't just CorsBlocked
.then(Unauthorized)],
['GET no CORS, object exists',
() => MakeRequest('GET', 'public-no-cors/obj')
.then(CorsBlocked)], // But req 200s
['GET no CORS, object does not exist',
() => MakeRequest('GET', 'public-no-cors/should-404')
.then(CorsBlocked)], // But req 404s
['GET Range no CORS',
() => MakeRequest('GET', 'public-no-cors/obj', { Range: 'bytes=100-199' })
.then(CorsBlocked)], // preflight fails
['GET other-referrer, object exists',
() => MakeRequest('GET', 'other-referrer-allowed/obj')
.then(CorsBlocked)], // But req 401s
['GET other-referrer, object does not exist',
() => MakeRequest('GET', 'other-referrer-allowed/should-404')
.then(CorsBlocked)], // But req 401s
['GET Range other-referrer',
() => MakeRequest('GET', 'other-referrer-allowed/obj', { Range: 'bytes=100-199' })
.then(CorsBlocked)], // preflight fails
['GET other-referrer, attempt to spoof referer',
() => MakeRequest('GET', 'other-referrer-allowed/obj', { Referer: 'https://other-host' })
.then(CorsBlocked)], // new header gets ignored, req 401s with no allow-origin
['GET no ACL, object exists',
() => MakeRequest('GET', 'private-with-cors/obj')
.then(Unauthorized)],
['GET no ACL, object does not exist',
() => MakeRequest('GET', 'private-with-cors/would-404')
.then(Unauthorized)],
['GET completely private',
() => MakeRequest('GET', 'private/obj')
.then(CorsBlocked)],
['GET Range completely private',
() => MakeRequest('GET', 'private/obj', { Range: 'bytes=100-199' })
.then(CorsBlocked)],
['GET referrer allowed',
() => MakeRequest('GET', 'referrer-allowed/obj')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(1024))],
['HEAD referrer allowed',
() => MakeRequest('HEAD', 'referrer-allowed/obj')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(HasNoBody)],
['GET Range referrer allowed',
() => MakeRequest('GET', 'referrer-allowed/obj', { Range: 'bytes=100-199' })
.then(HasStatus(206, 'Partial Content'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(100))],
['GET attempt to spoof referer',
() => MakeRequest('GET', 'referrer-allowed/obj', { Referer: 'https://other-host' })
// new header gets ignored, no preflight, get succeeds
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(BodyHasLength(1024))]
])

139
test/cors/test-symlink.js Normal file
View File

@ -0,0 +1,139 @@
import {
runTests,
MakeRequest,
HasStatus,
HasHeaders,
HasCommonResponseHeaders,
DoesNotHaveHeaders,
HasNoBody,
CorsBlocked,
Skip
} from './harness.js'
import { GetClusterInfo } from './test-info.js'
function MakeSymlinkRequest () {
return GetClusterInfo.then((clusterInfo) => {
if (!('symlink' in clusterInfo)) {
throw new Skip('Symlink is not enabled')
}
return MakeRequest(...arguments)
})
}
runTests('symlink', [
['GET link to no CORS',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-no-cors')
.then(CorsBlocked)],
['HEAD link to no CORS',
() => MakeSymlinkRequest('HEAD', 'public-with-cors/symlink-to-public-no-cors')
.then(CorsBlocked)],
['GET Range link to no CORS',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-no-cors', { Range: 'bytes=100-199' })
.then(CorsBlocked)], // But preflight *succeeded*!
['GET link with CORS',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-with-cors')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then((resp) => {
if (resp.responseText.length !== 1024) {
throw new Error('Expected body to have length 1024, got ' + resp.responseText.length)
}
})],
['HEAD link with CORS',
() => MakeSymlinkRequest('HEAD', 'public-with-cors/symlink-to-public-with-cors')
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then(HasNoBody)],
['GET Range link with CORS',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-with-cors', { Range: 'bytes=100-199' })
.then(HasStatus(206, 'Partial Content'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then((resp) => {
if (resp.responseText.length !== 100) {
throw new Error('Expected body to have length 100, got ' + resp.responseText.length)
}
})],
['GET private',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-private')
.then(CorsBlocked)], // TODO: maybe should be Unauthorized?
['HEAD private',
() => MakeSymlinkRequest('HEAD', 'public-with-cors/symlink-to-private')
.then(CorsBlocked)], // TODO: maybe should be Unauthorized?
['GET private Range',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-private', { Range: 'bytes=100-199' })
.then(CorsBlocked)], // TODO: maybe should be Unauthorized?
['GET If-Match matching',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-with-cors', { 'If-Match': '0f343b0931126a20f133d67c2b018a3b' })
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then((resp) => {
if (resp.responseText.length !== 1024) {
throw new Error('Expected body to have length 1024, got ' + resp.responseText.length)
}
})],
['GET If-Match not matching',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-with-cors', { 'If-Match': 'something-else' })
.then(HasStatus(412, 'Precondition Failed'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'text/html; charset=UTF-8',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then(HasNoBody)],
['GET If-None-Match matching',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-with-cors', { 'If-None-Match': '0f343b0931126a20f133d67c2b018a3b' })
.then(HasStatus(304, 'Not Modified'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
// Content-Type can vary depending on storage policy type...
// 'Content-Type': 'text/html; charset=UTF-8',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['Content-Type', 'X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then(HasNoBody)],
['GET If-None-Match not matching',
() => MakeSymlinkRequest('GET', 'public-with-cors/symlink-to-public-with-cors', { 'If-None-Match': 'something-else' })
.then(HasStatus(200, 'OK'))
.then(HasCommonResponseHeaders)
.then(HasHeaders({
'Content-Type': 'application/octet-stream',
Etag: '0f343b0931126a20f133d67c2b018a3b'
}))
.then(HasHeaders(['X-Object-Meta-Mtime']))
.then(DoesNotHaveHeaders(['Content-Location']))
.then((resp) => {
if (resp.responseText.length !== 1024) {
throw new Error('Expected body to have length 1024, got ' + resp.responseText.length)
}
})]
])

View File

@ -0,0 +1,30 @@
- hosts: all
become: true
tasks:
- name: install virtual frame buffer
yum:
name: xorg-x11-server-Xvfb
state: present
- name: install selenium
pip:
name: selenium
state: present
- name: install firefox
yum:
name: firefox
state: present
- name: fetch firefox driver
get_url:
url: https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz
dest: /tmp/geckodriver.tar.gz
- name: unpack firefox driver
unarchive:
src: /tmp/geckodriver.tar.gz
dest: /usr/local/bin
remote_src: true
- name: check firefox version
command: firefox --version
#- name: install chromium
# yum:
# name: chromium-headless
# state: present

View File

@ -0,0 +1,25 @@
- hosts: all
become: true
tasks:
- name: Copy geckodriver log from worker nodes to executor node
synchronize:
src: '{{ ansible_env.HOME }}/geckodriver.log'
dest: '{{ zuul.executor.log_root }}'
mode: pull
copy_links: true
verify_host: true
- name: Copy CORS tests output from worker nodes to executor node
synchronize:
src: '{{ ansible_env.HOME }}/cors-test-results.txt'
dest: '{{ zuul.executor.log_root }}'
mode: pull
copy_links: true
verify_host: true
- zuul_return:
data:
zuul:
artifacts:
- name: CORS test results
url: cors-test-results.txt

View File

@ -0,0 +1,15 @@
- hosts: all
tasks:
- name: Shutdown main swift services
shell: "swift-init stop main"
ignore_errors: true
- name: Start main swift services
shell: "swift-init start main"
- name: Run CORS tests
shell: >
xvfb-run python
{{ ansible_env.HOME }}/{{ zuul.project.src_dir }}/test/cors/main.py
--output {{ ansible_env.HOME }}/cors-test-results.txt
all