diff --git a/.gitignore b/.gitignore index cc6644032..1fafe3c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,8 +9,10 @@ build cover venv .venv +output.xml *.sublime-workspace *.sqlite +*.html .*.swp .DS_Store .testrepository diff --git a/README.rst b/README.rst index 60e987121..262dcdde6 100644 --- a/README.rst +++ b/README.rst @@ -1 +1,31 @@ -Marconi - Queue and Notification service for OpenStack +**Marconi - Queue and Notification service for OpenStack** + +*Steps to run Marconi server locally with MongoDB* + +1. `Install mongodb`_ locally +2. Start your local MongoDB instance:: + + mongod +3. Clone the Marconi repo:: + + git clone https://github.com/stackforge/marconi.git +4. cd to your local copy of the repo +5. Copy the Marconi config files to the directory ~/.marconi:: + + cp -r marconi/etc/*.conf-sample ~/.marconi +6. Update the [drivers:storage:mongodb] section in ~/.marconi/marconi.conf + + 6a. Comment out the uri pointing to replicaset:: + + ;uri = mongodb://db1.example.net,db2.example.net:2500/?replicaSet=test&ssl=true&w=majority + 6b. Add a new line with uri pointing to the local mongoDB instance:: + + uri = mongodb://localhost +7. Run the following command:: + + python setup.py develop +8. Start the marconi server:: + + marconi-server + +.. _`Install mongodb` : http://docs.mongodb.org/manual/installation/ \ No newline at end of file diff --git a/marconi/tests/system/README.rst b/marconi/tests/system/README.rst new file mode 100644 index 000000000..2c19ebed2 --- /dev/null +++ b/marconi/tests/system/README.rst @@ -0,0 +1,63 @@ +**Marconi System Tests** + +The System tests treat Marconi as a black box. +The API calls are made similar to how an user would make them. +Unlike unit tests, the system tests do not use mock endpoints. + +**Running the System Tests** + +1. Setup the Marconi server, to run the tests against. + Refer to the Marconi `README`_ on how to run Marconi locally. + (If you are running the tests against an existing server, skip this step.) + +2. System tests require the `requests`_ & `robot`_ packages. Run the following to install them :: + + pip install -r tools/system-test-requires + +3. cd to the marconi/tests/system directory + +4. Copy etc/system-tests.conf-sample to one of the following locations:: + + ~/.marconi/system-tests.conf + /etc/marconi/system-tests.conf + +5. Update the config file to point to the Marconi server you want to run the tests against + +6. If keystone auth is enabled, update system-tests.conf with the credentials. + +7. To run tests use the pybot commands, + + Run all test suites :: + + pybot marconi/tests/system/queue/queue_tests.txt marconi/tests/system/messages/messages_tests.txt marconi/tests/system/claim/claim_tests.txt + + Run a specific test suite :: + + pybot marconi/tests/system/queue/queue_tests.txt + + pybot marconi/tests/system/messages/messages_tests.txt + + pybot marconi/tests/system/claim/claim_tests.txt + + pybot will generate report.html & log.html after the test run is complete. + + +**To Add new tests** + + +1. Add test case definition to the robot test case file (queue/queue_tests.txt, messages/messages_tests.txt, claim/claim_tests.txt) + See `here`_ for more details on writing test cases. + +2. Add test data to the test_data.csv in the same directory as the test case file you updated above (eg. queue/test_data.csv) + +3. Add any validation logic you might need, to one of the following: + + * corresponing \*fnlib.py (eg. queue/queuefnlib.py) + * common/functionlib.py (If the code can be used across multiple test suites) + +.. _README : https://github.com/stackforge/marconi/blob/master/README.rst +.. _requests : https://pypi.python.org/pypi/requests +.. _robot : https://pypi.python.org/pypi/robotframework +.. _here : http://robotframework.googlecode.com/hg/doc/userguide/RobotFrameworkUserGuide.html?r=2.7.7#creating-test-cases + + diff --git a/marconi/tests/system/__init__.py b/marconi/tests/system/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/marconi/tests/system/claim/__init__.py b/marconi/tests/system/claim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/marconi/tests/system/claim/claim_tests.txt b/marconi/tests/system/claim/claim_tests.txt new file mode 100755 index 000000000..68d3d7e1d --- /dev/null +++ b/marconi/tests/system/claim/claim_tests.txt @@ -0,0 +1,66 @@ +| *Setting* | *Value* | +| Documentation | Marconi - Queue Test Suite | +| Library | ../common/http.py | +| Library | ../common/functionlib.py | +| Library | ../messages/msgfnlib.py | +| Library | Collections | +| Library | claimfnlib.py | +| Variables | getdata.py | +| Force Tags | CLAIM | +| Suite Setup | executetests | ${API_TEST_DATA[6]} | # Test Suite Setup - Creates a Queue +| Suite Teardown | executetests | ${API_TEST_DATA[8]} | # Test Suite Teardown - Deletes the queue created by setup + + +| *Test Case* | *Action* | *Argument* | *Argument* | *Argument* | # Comment +| 0:SUITE SETUP | [DOCUMENTATION] | | Post 500 messages | | #SUITE SETUP - POST MULTIPLE MESSAGES +| | [Tags] | INSERT_MESSAGE | | | #(Robot allows only one keyword in setup) +| | ${reqparam}= | Create Dictionary | messagecount | ${50} | # Specify count of messages to be posted +| | | ... | ttl | ${300} | # Specify count of messages to be posted +| | ${msgbody}= | dummygetmessagebody | ${reqparam} | | # Gets the message body to post +| | Set To Dictionary | ${API_TEST_DATA[7]} | body | ${msgbody} | # Set the POST body +| | :FOR | ${index} | IN RANGE | 10 | # Loop to post 50 messages * 10 times +| | | executetests | ${API_TEST_DATA[7]} | | # postresponse = [httpheaders,httpresponsebod] +| 1:CLAIM 2 MESSAGES | [DOCUMENTATION] | Claim messages | | #TEST CASE 1 - CLAIM 2 MESSAGES +| | ... | with limit = 2 | | +| | @{postresponse}= | executetests | ${API_TEST_DATA[0]} | # Post a claim; postresponse = [httpheaders, httpresponsebody] +| | verifyclaimmsg | ${2} | @{postresponse} | +| 2:CLAIM 5 MESSAGES | [DOCUMENTATION] | Claim messages | | #TEST CASE 2 - CLAIM 5 MESSAGES +| | ... | with limit = 5 | | +| | @{postresponse}= | executetests | ${API_TEST_DATA[1]} | # Post a claim; postresponse = [httpheaders, httpresponsebody] +| | verifyclaimmsg | ${5} | @{postresponse} | +| 3:CLAIM MESSAGES | [DOCUMENTATION] | Claim messages | | #TEST CASE 3 - CLAIM DEFAULT # OF MESSAGES +| | ... | with no params | | (currently 10) +| | @{postresponse}= | executetests | ${API_TEST_DATA[2]} | # postresponse = [httpheaders, httpresponsebody] +| | verifyclaimmsg | ${10} | @{postresponse} | +| 4:CLAIM 15 MESSAGE | [DOCUMENTATION] | Claim messages | | #TEST CASE 4 - CLAIM 15 MESSAGES +| | ... | with limit = 15 | | +| | @{postresponse}= | executetests | ${API_TEST_DATA[3]} | # Post a claim; postresponse = [httpheaders, httpresponsebody] +| | verifyclaimmsg | ${15} | @{postresponse} | +| 5:CLAIM 55 MESSAGE | [DOCUMENTATION] | Claim messages | | #TEST CASE 5 - CLAIM 55 MESSAGES +| | ... | with limit = 55 | | +| | @{postresponse}= | executetests | ${API_TEST_DATA[4]} | # Post a claim; postresponse = [httpheaders, httpresponsebody] +| | verifyclaimmsg | ${50} | @{postresponse} | # MAXIMUM MESSAGES RETURNED IS CURRENTLY 50 +| 6: PATCH CLAIM | [DOCUMENTATION] | Patch a claim | | # TEST CASE 6 - UPDATE CLAIM +| | @{postresponse}= | executetests | ${API_TEST_DATA[5]} | # Post a claim; postresponse = [httpheaders, httpresponsebody] +| | patchclaim | @{postresponse} | | # Patch the above claim +| 7: DELETE MESSAGE | [DOCUMENTATION] | Delete message | | # TEST CASE 7 - DELETE A CLAIMED MESSAGE +| | ... | with claim id | | +| | @{postresponse}= | executetests | ${API_TEST_DATA[5]} | # Post a claim +| | deleteclaimedmsgs | @{postresponse} | | # Delete messages returned in the above claim +| 8: PATCH EXPIRED CLAIM | [DOCUMENTATION] | Patch expired claim | | # TEST CASE 8 - UPDATE EXPIRED CLAIM +| | @{postresponse}= | executetests | ${API_TEST_DATA[9]} | # Post a claim with TTL= 1 sec +| | Sleep | 3s | | +| | patchclaim | @{postresponse} | | # Patch the above claim +| 9: DELETE MESSAGE ON EXPIRED CLAIM | [DOCUMENTATION] | Delete message | # TEST CASE 9 - DELETE MESSAGE ON AN EXPIRED CLAIM +| | ... | on expired claim | +| | @{postresponse}= | executetests | ${API_TEST_DATA[10]} | # Post a claim with TTL= 1 sec +| | Sleep | 3s | | +| | deleteclaimedmsgs | @{postresponse} | | # Delete message returned in the above claim +| 10: RELEASE CLAIM | [DOCUMENTATION] | Release claim | | # TEST CASE 10 - RELEASE CLAIM +| | @{postresponse}= | executetests | ${API_TEST_DATA[11]} | # Post a claim with TTL= 1 sec +| | releaseclaim | @{postresponse} | | # Patch the above claim +| 11: GET MESSAGE FROM EXPIRED CLAIM | [DOCUMENTATION] | Get message | # TEST CASE 11 - GET MESSAGE FROM EXPIRED CLAIM +| | ... | from expired claim | +| | @{postresponse}= | executetests | ${API_TEST_DATA[12]} | # Post a claim with TTL= 1 sec +| | Sleep | 3s | | +| | getclaimedmsgs | @{postresponse} | | # Delete message returned in the above claim \ No newline at end of file diff --git a/marconi/tests/system/claim/claimfnlib.py b/marconi/tests/system/claim/claimfnlib.py new file mode 100644 index 000000000..99f7e6ca1 --- /dev/null +++ b/marconi/tests/system/claim/claimfnlib.py @@ -0,0 +1,239 @@ +# Copyright (c) 2013 Rackspace, 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 json + +from marconi.tests.system.common import functionlib +from marconi.tests.system.common import http + + +def verify_claim_msg(count, *claim_response): + """Verifies claim messages. + + Validation steps include - verifying the + 1. number of messages returned is <= limit specified + 2. query claim & verifying the response + :param count: limit specified in the claim request + :param claim_response : response returned for the claim request + """ + msg_length_flag = False + + headers = claim_response[0] + body = claim_response[1] + + msg_length_flag = verify_claim_msglength(count, body) + if msg_length_flag: + query_claim(headers, body) + else: + assert msg_length_flag, "More msgs returned than specified in limit" + + +def verify_claim_msglength(count, *body): + """Validates that number of messages returned is <= limit specified.""" + msg_list = body + msg_list = json.loads(msg_list[0]) + return (len(msg_list) <= count) + + +def query_claim(headers, *body): + """Verifies the response of post claim. + + Does a Query Claim using the href in post claim. + Compares the messages returned in Query claim with the messages + returned on Post Claim. + """ + test_result_flag = False + + msg_list = body[0] + msg_list = json.loads(msg_list) + + location = headers["Location"] + url = functionlib.create_url_from_appender(location) + header = functionlib.create_marconi_headers() + + get_msg = http.get(url, header) + if get_msg.status_code == 200: + query_body = json.loads(get_msg.text) + query_msgs = query_body["messages"] + test_result_flag = verify_query_msgs(query_msgs, msg_list) + + if test_result_flag: + return test_result_flag + else: + print "URL" + print url + print "HEADER" + print header + print "Messages returned by Query Claim" + print query_msgs + print "# of Messages returned by Query Claim", len(query_msgs) + print 'Messages returned by Claim Messages' + print msg_list + print "# of Messages returned by Claim messages", len(msg_list) + assert test_result_flag, "Query Claim Failed" + + +def verify_query_msgs(querymsgs, msg_list): + """Verifies response from Query claim. + + Compares the messages returned in Query Claim with the messages + returned when the claim was posted. + """ + test_result_flag = True + idx = 0 + + for msg in querymsgs: + if ((msg["body"] != msg_list[idx]["body"]) or + (msg["href"] != msg_list[idx]["href"]) or + (msg["ttl"] != msg_list[idx]["ttl"])): + test_result_flag = False + idx = idx + 1 + + return test_result_flag + + +def patch_claim(*claim_response): + """Patches a claim & verifies the results. + + Extracts claim id from the POST response input & updates the claim. + If PATCH claim succeeds, verifies that the claim TTL is extended. + """ + test_result_flag = False + + headers = claim_response[0] + location = headers["Location"] + url = functionlib.create_url_from_appender(location) + header = functionlib.create_marconi_headers() + + ttl_value = 300 + payload = '{ "ttl": ttlvalue }' + payload = payload.replace("ttlvalue", str(ttl_value)) + + patch_response = http.patch(url, header, body=payload) + if patch_response.status_code == 204: + test_result_flag = verify_patch_claim(url, header, ttl_value) + else: + print "Patch HTTP Response code: {}".format(patch_response.status_code) + print patch_response.headers + print patch_response.text + assert test_result_flag, "Patch Claim Failed" + + if not test_result_flag: + assert test_result_flag, "Query claim after the patch failed" + + +def verify_patch_claim(url, header, ttl_extended): + """Verifies if patch claim was successful. + + The following steps are performed for the verification. + 1. GETs the claim + 2. Checks tht the actual claim TTL value is > TTL in the patch request + + :param ttl_extended : TTL posted in the patch request. + """ + test_result_flag = True + + get_claim = http.get(url, header) + response_body = json.loads(get_claim.text) + + ttl = response_body["ttl"] + if ttl < ttl_extended: + print get_claim.status_code + print get_claim.headers + print get_claim.text + test_result_flag = False + + return test_result_flag + + +def create_urllist_fromhref(*response): + """EXtracts href & creates a url list. + + :param *response : http response text with the list of messages. + """ + rspbody = json.loads(response[1]) + urllist = [functionlib.create_url_from_appender(item["href"]) + for item in rspbody] + return urllist + + +def delete_claimed_msgs(*claim_response): + """Deletes claimed messages. + + Verifies that the deletes were successful with a GET on the deleted msg. + """ + test_result_flag = False + + urllist = create_urllist_fromhref(*claim_response) + header = functionlib.create_marconi_headers() + + for url in urllist: + delete_response = http.delete(url, header) + if delete_response.status_code == 204: + test_result_flag = functionlib.verify_delete(url, header) + else: + print "DELETE message with claim ID: {}".format(url) + print delete_response.status_code + print delete_response.headers + print delete_response.text + assert test_result_flag, "Delete Claimed Message Failed" + + if not test_result_flag: + assert test_result_flag, "Get message after DELETE did not return 404" + + +def get_claimed_msgs(*claim_response): + """Does get on all messages returned in the claim.""" + test_result_flag = True + + urllist = create_urllist_fromhref(*claim_response) + header = functionlib.create_marconi_headers() + + for url in urllist: + get_response = http.get(url, header) + if get_response.status_code != 200: + print url + print header + print "Get Response Code: {}".format(get_response.status_code) + test_result_flag = False + + if not test_result_flag: + assert test_result_flag, "Get Claimed message Failed" + + +def release_claim(*claim_response): + """Deletes claim & verifies the delete was successful. + + Extracts claim id from the POST response input & deletes the claim. + If DELETE claim succeeds, verifies that a GET claim returns 404. + """ + test_result_flag = False + + headers = claim_response[0] + location = headers["Location"] + url = functionlib.create_url_from_appender(location) + header = functionlib.create_marconi_headers() + + release_response = http.delete(url, header) + if release_response.status_code == 204: + test_result_flag = functionlib.verify_delete(url, header) + else: + print "Release Claim HTTP code:{}".format(release_response.status_code) + print release_response.headers + print release_response.text + assert test_result_flag, "Release Claim Failed" + + if not test_result_flag: + assert test_result_flag, "Get claim after the release failed" diff --git a/marconi/tests/system/claim/getdata.py b/marconi/tests/system/claim/getdata.py new file mode 100755 index 000000000..7408cf98a --- /dev/null +++ b/marconi/tests/system/claim/getdata.py @@ -0,0 +1,37 @@ +# Copyright (c) 2013 Rackspace, 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 csv + +from marconi.tests.system.common import config +from marconi.tests.system.common import functionlib + + +cfg = config.Config() + + +def get_data(): + """Reads the test data from claim/test_data.csv.""" + DATA = [] + with open('marconi/tests/system/claim/test_data.csv', 'rb') as datafile: + testdata = csv.DictReader(datafile, delimiter='|') + for row in testdata: + DATA.append(row) + for row in DATA: + row['header'] = functionlib.get_headers(row['header']) + row['url'] = row['url'].replace("", cfg.base_url) + return DATA + +API_TEST_DATA = get_data() diff --git a/marconi/tests/system/claim/test_data.csv b/marconi/tests/system/claim/test_data.csv new file mode 100755 index 000000000..2e9fa52a3 --- /dev/null +++ b/marconi/tests/system/claim/test_data.csv @@ -0,0 +1,15 @@ +TestID|httpverb|url|header|body|params|expectedRC|expectedResponseBody +1|POST |/queues/claimtestqueue/claims?limit=2||{"ttl": 50, "grace": 60}||200| +2|POST |/queues/claimtestqueue/claims?limit=5||{"ttl": 50, "grace": 60}||200| +3|POST |/queues/claimtestqueue/claims||{"ttl": 50, "grace": 60}||200| +4|POST |/queues/claimtestqueue/claims?limit=15||{"ttl": 50, "grace": 60}||200| +5|POST |/queues/claimtestqueue/claims?limit=55||{"ttl": 50, "grace": 60}||200| +6|POST |/queues/claimtestqueue/claims?limit=4||{"ttl": 50, "grace": 60}||200| +7|PUT |/queues/claimtestqueue||{"ttl": 50, "grace": 60}||201| +0|POST |/queues/claimtestqueue/messages||||201| +0|DELETE |/queues/claimtestqueue||||204| +8|POST |/queues/claimtestqueue/claims?limit=2||{"ttl": 1, "grace": 1}||200| +9|POST |/queues/claimtestqueue/claims?limit=2||{"ttl": 1, "grace": 1}||200| +10|POST |/queues/claimtestqueue/claims?limit=2||{"ttl": 50, "grace": 60}||200| +11|POST |/queues/claimtestqueue/claims?limit=2||{"ttl": 1, "grace": 1}||200 +11|GET |||||200 \ No newline at end of file diff --git a/marconi/tests/system/common/__init__.py b/marconi/tests/system/common/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/marconi/tests/system/common/config.py b/marconi/tests/system/common/config.py new file mode 100644 index 000000000..36c150caa --- /dev/null +++ b/marconi/tests/system/common/config.py @@ -0,0 +1,71 @@ +# Copyright (c) 2013 Rackspace, 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 ConfigParser +import os +import uuid + + +class Config(object): + def __init__(self, config_path=None): + if config_path is None: + if os.path.exists('/etc/marconi/system-tests.conf'): + config_path = '/etc/marconi/system-tests.conf' + else: + config_path = os.path.expanduser('~/.marconi' + '/system-tests.conf') + self.parser = ConfigParser.SafeConfigParser() + self.parser.read(config_path) + + @property + def auth_enabled(self): + return self.parser.getboolean('auth', 'auth_on') + + @property + def username(self): + return self.parser.get('auth', 'username') + + @property + def password(self): + return self.parser.get('auth', 'password') + + @property + def base_server(self): + return self.parser.get('marconi_env', 'marconi_url') + + @property + def marconi_version(self): + return self.parser.get('marconi_env', 'marconi_version') + + @property + def tenant_id(self): + return self.parser.get('marconi_env', 'tenant_id') + + @property + def base_url(self): + return (self.base_server + '/' + self.marconi_version + + '/' + self.tenant_id) + + @property + def uuid(self): + return str(uuid.uuid1()) + + @property + def user_agent(self): + return self.parser.get('header_values', 'useragent') + + @property + def host(self): + return self.parser.get('header_values', 'host') diff --git a/marconi/tests/system/common/functionlib.py b/marconi/tests/system/common/functionlib.py new file mode 100644 index 000000000..ae02f0029 --- /dev/null +++ b/marconi/tests/system/common/functionlib.py @@ -0,0 +1,200 @@ +# Copyright (c) 2013 Rackspace, 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 binascii +import json +import os + +from marconi.tests.system.common import config +from marconi.tests.system.common import http + + +cfg = config.Config() + + +def get_keystone_token(): + """Gets Keystone Auth token.""" + req_json = { + "auth": { + "passwordCredentials": { + "username": cfg.username, + "password": cfg.password + } + }, + } + + header = '{"Host": "identity.api.rackspacecloud.com",' + header += '"Content-Type": "application/json","Accept":"application/json"}' + url = cfg.auth_url + + response = http.post(url=url, header=header, body=req_json) + response_body = json.loads(response.text) + + auth_token = response_body["access"]["token"]["id"] + return auth_token + + +def get_auth_token(): + """Returns a valid auth token if auth is turned on.""" + if cfg.auth_enabled == "true": + auth_token = get_keystone_token() + else: + auth_token = "notrealtoken" + return auth_token + + +def create_marconi_headers(): + """Returns headers to be used for all Marconi requests.""" + auth_token = get_auth_token() + + headers = '{"Host": "","User-Agent": "","Date":"",' + headers += '"Accept": "application/json","Accept-Encoding": "gzip",' + headers += '"X-Auth-Token": "","Client-ID": ""}' + headers = headers.replace("", auth_token) + headers = headers.replace("", cfg.host) + headers = headers.replace("", cfg.user_agent) + headers = headers.replace("", cfg.uuid) + + return headers + + +def invalid_auth_token_header(): + """Returns a header with invalid auth token.""" + auth_token = get_auth_token() + + headers = '{"Host":"","User-Agent":"","Date":"",' + headers += '"Accept": "application/json","Accept-Encoding": "gzip",' + headers += '"X-Auth-Token": ""}' + headers = headers.replace("", auth_token) + headers = headers.replace("", cfg.host) + headers = headers.replace("", cfg.user_agent) + + return headers + + +def missing_header_fields(): + """Returns a header with missing USER_AGENT header.""" + auth_token = get_auth_token() + + headers = '{"Host": "","Date": "",' + headers += '"Accept": "application/json","Accept-Encoding": "gzip",' + headers += '"X-Auth-Token": ""}' + headers = headers.replace("", auth_token) + headers = headers.replace("", cfg.host) + + return headers + + +def plain_text_in_header(): + """Returns headers to be used for all Marconi requests.""" + auth_token = get_auth_token() + + headers = '{"Host":"","User-Agent":"","Date":"",' + headers += '"Accept": "text/plain","Accept-Encoding": "gzip",' + headers += '"X-Auth-Token": ""}' + headers = headers.replace("", auth_token) + headers = headers.replace("", cfg.host) + headers = headers.replace("", cfg.user_agent) + + return headers + + +def asterisk_in_header(): + """Returns headers to be used for all Marconi requests.""" + auth_token = get_auth_token() + + headers = '{"Host":"","User-Agent":"","Date":"",' + headers += '"Accept": "*/*","Accept-Encoding": "gzip",' + headers += '"X-Auth-Token": ""}' + headers = headers.replace("", auth_token) + headers = headers.replace("", cfg.host) + headers = headers.replace("", cfg.user_agent) + + return headers + + +def get_headers(input_header): + """Creates http request headers. + + 1. If header value is specified in the test_data.csv, that will be used. + 2. Headers can also be substituted in the Robot test case definition + file (*_tests.txt) + 3. If 1. & 2. is not true --> + Replaces the header data with generic Marconi headers. + """ + if input_header: + header = input_header + else: + header = create_marconi_headers() + return header + + +def get_custom_body(kwargs): + """Returns a custom request body.""" + req_body = {"data": ""} + if "metadatasize" in kwargs.keys(): + random_data = binascii.b2a_hex(os.urandom(kwargs["metadatasize"])) + req_body["data"] = random_data + return json.dumps(req_body) + + +def create_url_from_appender(appender): + """Returns complete url using the appender (with a a preceding '/').""" + next_url = str(cfg.base_server + appender) + return(next_url) + + +def get_url_from_location(header): + """returns : the complete url referring to the location.""" + location = header["location"] + url = create_url_from_appender(location) + return url + + +def verify_metadata(get_data, posted_body): + """@todo(malini) - Really verify the metadata.""" + test_result_flag = False + + get_data = str(get_data) + posted_body = str(posted_body) + print(get_data, type(get_data)) + print(posted_body, type(posted_body)) + if get_data in posted_body: + print("AYYY") + else: + test_result_flag = False + print("NAYYY") + + return test_result_flag + + +def verify_delete(url, header): + """Verifies the DELETE was successful, with a GET on the deleted item.""" + test_result_flag = False + + getmsg = http.get(url, header) + if getmsg.status_code == 404: + test_result_flag = True + else: + print("GET after DELETE failed") + print("URL") + print url + print("headers") + print header + print("Response Body") + print getmsg.text + assert test_result_flag, "GET Code {}".format(getmsg.status_code) + + return test_result_flag diff --git a/marconi/tests/system/common/http.py b/marconi/tests/system/common/http.py new file mode 100755 index 000000000..da4fe9cd9 --- /dev/null +++ b/marconi/tests/system/common/http.py @@ -0,0 +1,176 @@ +# Copyright (c) 2013 Rackspace, 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 json +import requests + + +def get(url, header='', param=''): + """Does http GET.""" + if header: + header = json.loads(header) + try: + response = requests.get(url, headers=header, params=param) + except requests.ConnectionError as detail: + print("ConnectionError: Exception in http.get {}".format(detail)) + except requests.HTTPError as detail: + print("HTTPError: Exception in http.get {}".format(detail)) + except requests.Timeout as detail: + print("Timeout: Exception in http.get {}".format(detail)) + except requests.TooManyRedirects as detail: + print("TooManyRedirects: Exception in http.get {}".format(detail)) + return response + + +def post(url, header='', body='', param=''): + """Does http POST.""" + if header: + header = json.loads(header) + body = str(body) + body = body.replace("'", '"') + try: + response = requests.post(url, headers=header, data=body, + params=param) + except requests.ConnectionError as detail: + print("ConnectionError: Exception in http.post {}".format(detail)) + except requests.HTTPError as detail: + print("HTTPError: Exception in http.post {}".format(detail)) + except requests.Timeout as detail: + print("Timeout: Exception in http.post {}".format(detail)) + except requests.TooManyRedirects as detail: + print("TooManyRedirects: Exception in http.post {}".format(detail)) + return response + + +def put(url, header='', body='', param=''): + """Does http PUT.""" + response = None + if header: + header = json.loads(header) + + try: + response = requests.put(url, headers=header, data=body, + params=param) + except requests.ConnectionError as detail: + print("ConnectionError: Exception in http.put {}".format(detail)) + except requests.HTTPError as detail: + print("HTTPError: Exception in http.put {}".format(detail)) + except requests.Timeout as detail: + print("Timeout: Exception in http.put {}".format(detail)) + except requests.TooManyRedirects as detail: + print("TooManyRedirects: Exception in http.put {}".format(detail)) + return response + + +def delete(url, header='', param=''): + """Does http DELETE.""" + response = None + if header: + header = json.loads(header) + + try: + response = requests.delete(url, headers=header, params=param) + except requests.ConnectionError as detail: + print("ConnectionError: Exception in http.delete {}".format(detail)) + except requests.HTTPError as detail: + print("HTTPError: Exception in http.delete {}".format(detail)) + except requests.Timeout as detail: + print("Timeout: Exception in http.delete {}".format(detail)) + except requests.TooManyRedirects as detail: + print("TooManyRedirects: Exception in http.delete {}".format(detail)) + return response + + +def patch(url, header='', body='', param=''): + """Does http PATCH.""" + response = None + if header: + header = json.loads(header) + + try: + response = requests.patch(url, headers=header, data=body, + params=param) + except requests.ConnectionError as detail: + print("ConnectionError: Exception in http.patch {}".format(detail)) + except requests.HTTPError as detail: + print("HTTPError: Exception in http.patch {}".format(detail)) + except requests.Timeout as detail: + print("Timeout: Exception in http.patch {}".format(detail)) + except requests.TooManyRedirects as detail: + print("TooManyRedirects: Exception in http.patch {}".format(detail)) + return response + + +def executetests(row): + """Entry Point for all tests. + + Executes the tests defined in the *_tests.txt, + using the test data from *_data.csv. + """ + http_verb = row['httpverb'].strip() + url = row['url'] + header = row['header'] + params = row['params'] + body = row['body'] + expected_RC = row['expectedRC'] + expected_RC = int(expected_RC) + expected_response_body = row['expectedResponseBody'] + + response = None + + if http_verb == 'GET': + response = get(url, header, params) + elif http_verb == 'POST': + response = post(url, header, body, params) + elif http_verb == 'PUT': + response = put(url, header, body, params) + elif http_verb == 'DELETE': + response = delete(url, header, params) + elif http_verb == 'PATCH': + response = patch(url, header, body, params) + + if response is not None: + test_result_flag = verify_response(response, expected_RC) + else: + test_result_flag = False + + if test_result_flag: + return response.headers, response.text + else: + print http_verb + print url + print header + print body + print "Actual Response: {}".format(response.status_code) + print "Actual Response Headers" + print response.headers + print"Actual Response Body" + print response.text + print"ExpectedRC: {}".format(expected_RC) + print"expectedresponsebody: {}".format(expected_response_body) + assert test_result_flag, "Actual Response does not match the Expected" + + +def verify_response(response, expected_RC): + """Compares the http Response code with the expected Response code.""" + test_result_flag = True + actual_RC = response.status_code + actual_response_body = response.text + if actual_RC != expected_RC: + test_result_flag = False + print("Unexpected http Response code {}".format(actual_RC)) + print "Response Body returned" + print actual_response_body + return test_result_flag diff --git a/marconi/tests/system/etc/__init__.py b/marconi/tests/system/etc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/marconi/tests/system/etc/system-tests.conf-sample b/marconi/tests/system/etc/system-tests.conf-sample new file mode 100644 index 000000000..cf064966c --- /dev/null +++ b/marconi/tests/system/etc/system-tests.conf-sample @@ -0,0 +1,15 @@ +[auth] +auth_on = true +#auth endpoint - url to get the auth token +url = https://identity.xxx.xxxx.com/v2.0/tokens +username = user +password = secret + +[marconi_env] +marconi_url = http://166.78.143.130:80 +marconi_version = v1 +tenant_id = 1 + +[header_values] +host = marconi.test.com +useragent = systemtests \ No newline at end of file diff --git a/marconi/tests/system/messages/__init__.py b/marconi/tests/system/messages/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/marconi/tests/system/messages/getdata.py b/marconi/tests/system/messages/getdata.py new file mode 100755 index 000000000..2afe55dc9 --- /dev/null +++ b/marconi/tests/system/messages/getdata.py @@ -0,0 +1,38 @@ +# Copyright (c) 2013 Rackspace, 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 csv + +from marconi.tests.system.common import config +from marconi.tests.system.common import functionlib + + +cfg = config.Config() + + +def get_data(): + """Gets Test Data from a csv file.""" + DATA = [] + with open('marconi/tests/system/messages/test_data.csv', 'rb') as datafile: + test_data = csv.DictReader(datafile, delimiter='|') + for row in test_data: + DATA.append(row) + for row in DATA: + row['header'] = functionlib.get_headers(row['header']) + row['url'] = row['url'].replace("", cfg.base_url) + return DATA + + +API_TEST_DATA = get_data() diff --git a/marconi/tests/system/messages/messages_tests.txt b/marconi/tests/system/messages/messages_tests.txt new file mode 100755 index 000000000..815620b66 --- /dev/null +++ b/marconi/tests/system/messages/messages_tests.txt @@ -0,0 +1,53 @@ +| *Setting* | *Value* | +| Documentation | Marconi - Message Test Suite | +| Library | ../common/http.py | +| Library | ../common/functionlib.py | +| Library | msgfnlib.py | +| Library | Collections | +| Variables | getdata.py | +| Force Tags | MESSAGES | +| Suite Setup | executetests | ${API_TEST_DATA[0]} | # Test Suite Setup - Creates a Queue +| Suite Teardown | executetests | ${API_TEST_DATA[7]} | # Test Suite Teardown - Deletes the queue created by setup + +| *Test Case* | *Action* | *Argument* | *Argument* | *Argument* | # Comment +| 1:POST SINGLE MESSAGE | [DOCUMENTATION] | | Post single message | | #TEST CASE 1 - POST SINGLE MESSAGE +| | [Tags] | INSERT_MESSAGE | | | +| | ${reqparam}= | Create Dictionary | messagecount | ${1} | # test Setup- Specify count of messages to be posted +| | ${msgbody}= | dummygetmessagebody | ${reqparam} | | # test Setup- Get the message body to post +| | Set To Dictionary | ${API_TEST_DATA[1]} | body | ${msgbody} | # test Setup -Set the POST body +| | @{postresponse}= | executetests | ${API_TEST_DATA[1]} | | # postresponse = [httpheaders, httpresponsebody] | +| | verifypostmsg | ${postresponse[0]} | ${API_TEST_DATA[1]["body"]} | | # GET the posted message & verify metadata +| | +| 2:POST MULTIPLE MESSAGES | [DOCUMENTATION] | | Post 50 messages | | #TEST CASE 2 - POST MULTIPLE MESSAGES +| | [Tags] | INSERT_MESSAGE | | | +| | ${reqparam}= | Create Dictionary | messagecount | ${50} | # Specify count of messages to be posted +| | ${msgbody}= | dummygetmessagebody | ${reqparam} | | # Gets the message body to post +| | Set To Dictionary | ${API_TEST_DATA[2]} | body | ${msgbody} | # Set the POST body +| | @{postresponse}= | executetests | ${API_TEST_DATA[2]} | | # postresponse = [httpheaders, httpresponsebody] +#| | verifypostmsg | ${postresponse[0]} | ${API_TEST_DATA[2]["body"]} | | # GET the posted messages & verify metadata +| | +| 3:GET MESSAGES-no params | [DOCUMENTATION] | Get message with no params | | # TEST CASE 3 - GET MESSAGE WITH NO PARAMS +| | @{getresponse}= | executetests | ${API_TEST_DATA[3]} | +| | verifygetmsgs | ${10} | @{getresponse} | # Verifies that number of messages returned is <= 10 +| | +| 4:GET MESSAGES-limit=5 | [DOCUMENTATION] | Get message with limit = 5 | | # TEST CASE 4 - GET MESSAGE WITH limit = 5 +| | @{getresponse}= | executetests | ${API_TEST_DATA[4]} | +| | verifygetmsgs | ${5} | @{getresponse} | # Verifies that number of messages returned is <= 5 +| 5:GET MESSAGES-echo=False | [DOCUMENTATION] | Get message with echo=False | | # TEST CASE 5 - GET MESSAGE WITH echo = False +| | @{getresponse}= | executetests | ${API_TEST_DATA[5]} | +| | verifygetmsgs | ${10} | @{getresponse} | # Verifies that number of messages returned is <= 10 +| 6:DELETE MESSAGE | [DOCUMENTATION] | | Delete message | | #TEST CASE 6 - DELETE MESSAGE +| | [Tags] | INSERT_MESSAGE | | | +| | ${reqparam}= | Create Dictionary | messagecount | ${1} | # test Setup- Specify count of messages to be posted +| | ${msgbody}= | dummygetmessagebody | ${reqparam} | | # test Setup- Get the message body to post +| | Set To Dictionary | ${API_TEST_DATA[1]} | body | ${msgbody} | # test Setup -Set the POST body +| | @{postresponse}= | executetests | ${API_TEST_DATA[1]} | | # postresponse = [httpheaders, httpresponsebody] | +| | deletemsg | ${postresponse[0]} | | | # GET the posted message & verify metadata +| 7:POST 60 MESSAGES | [DOCUMENTATION] | POST > MAX NUMBER OF MESSAGES | | | #TEST CASE 7 - POST > 50 MESSAGES +| | ... | ALLOWED PER POST (currently 50) | | | +| | [Tags] | INSERT_MESSAGE | | | +| | ${reqparam}= | Create Dictionary | messagecount | ${60} | # test Setup- Specify count of messages to be posted +| | ${msgbody}= | dummygetmessagebody | ${reqparam} | | # test Setup- Get the message body to post +| | Set To Dictionary | ${API_TEST_DATA[6]} | body | ${msgbody} | # test Setup -Set the POST body +| | @{postresponse}= | executetests | ${API_TEST_DATA[6]} | | # postresponse = [httpheaders, httpresponsebody] | +| | verifypostmsg | ${postresponse[0]} | ${API_TEST_DATA[6]["body"]} | | # GET the posted message & verify metadata diff --git a/marconi/tests/system/messages/msgfnlib.py b/marconi/tests/system/messages/msgfnlib.py new file mode 100644 index 000000000..eee2a53ec --- /dev/null +++ b/marconi/tests/system/messages/msgfnlib.py @@ -0,0 +1,201 @@ +# Copyright (c) 2013 Rackspace, 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. + +from __future__ import with_statement +import json +import random + +from marconi.tests.system.common import config +from marconi.tests.system.common import functionlib +from marconi.tests.system.common import http + + +cfg = config.Config() + + +def generate_dict(dict_length): + """Returns dictionary of specified length. Key:Value is random data.""" + with open('/usr/share/dict/words', 'rt') as f: + words = f.readlines() + words = [w.rstrip() for w in words] + + dict = {} + while len(dict) < dict_length: + key, value = random.sample(words, 2) + dict.update({key: value}) + return dict + + +def single_message_body(**kwargs): + """Returns message body for one message . + + The ttl will be a random value (60 <= TTL <= 1209600). + The message body will be random dict. + :param **kwargs can be {messagesize: x} , where x is message size + :param **kwargs can be {ttl: x} , where x is ttl in seconds + """ + valid_ttl = random.randint(60, 1209600) + + if "messagesize" in kwargs.keys(): + body = generate_dict(kwargs["messagesize"]) + else: + body = generate_dict(2) + + if "ttl" in kwargs.keys(): + ttl = kwargs["ttl"] + else: + ttl = valid_ttl + + message_body = {"ttl": ttl, "body": body} + return message_body + + +def get_message_body(**kwargs): + """Returns request body for post message tests. + + :param **kwargs can be {messagecount: x} , where x is the # of messages. + """ + message_count = kwargs["messagecount"] + multiple_message_body = [] + i = 0 + while i < message_count: + message_body = single_message_body(**kwargs) + multiple_message_body.append(message_body) + i = i + 1 + return multiple_message_body + + +def dummyget_message_body(dict): + """Dummy function since Robot framework does not support **kwargs.""" + dict = get_message_body(**dict) + return dict + + +def create_url(base_url=cfg.base_url, *msg_id_list): + """Creates url list for retrieving messages with message id.""" + url = [(base_url + msg_id) for msg_id in msg_id_list] + return url + + +def verify_msg_length(count=10, *msg_list): + """Verifies the number of messages returned. + + :param count: limit specified in the GET url. + :param *msg_list : list of message returned in the GET. + """ + test_result_flag = False + msg_body = json.loads(msg_list[0]) + msg_list = msg_body["messages"] + msg_count = len(msg_list) + if (msg_count <= count): + test_result_flag = True + else: + return test_result_flag + return test_result_flag + + +def get_href(*msg_list): + """Extracts href.""" + msg_body = json.loads(msg_list[0]) + link = msg_body["links"] + href = link[0]["href"] + return href + + +def verify_post_msg(msg_headers, posted_body): + """Verifies the response of POST Message(s). + + Retrieves the posted Message(s) & validates the message metadata. + """ + test_result_flag = False + + location = msg_headers['location'] + url = functionlib.create_url_from_appender(location) + header = functionlib.create_marconi_headers() + + getmsg = http.get(url, header) + if getmsg.status_code == 200: + test_result_flag = functionlib.verify_metadata(getmsg.text, + posted_body) + else: + print("Failed to GET {}".format(url)) + print("Request Header") + print header + print("Response Headers") + print getmsg.headers + print("Response Body") + print getmsg.text + assert test_result_flag, "HTTP code {}".format(getmsg.status_code) + + +def get_next_msgset(responsetext): + """Follows the href path & GETs the next batch of messages recursively.""" + test_result_flag = False + + href = get_href(responsetext) + url = functionlib.create_url_from_appender(href) + header = functionlib.create_marconi_headers() + + getmsg = http.get(url, header) + if getmsg.status_code == 200: + return get_next_msgset(getmsg.text) + elif getmsg.status_code == 204: + test_result_flag = True + return test_result_flag + else: + test_result_flag = False + print("Failed to GET {}".format(url)) + print(getmsg.text) + assert test_result_flag, "HTTP code {}".format(getmsg.status_code) + + +def verify_get_msgs(count, *getresponse): + """Verifies GET message & does a recursive GET if needed.""" + test_result_flag = False + + body = getresponse[1] + + msglengthflag = verify_msg_length(count, body) + if msglengthflag: + test_result_flag = get_next_msgset(body) + else: + print("Messages returned exceed requested number of messages") + test_result_flag = False + + if not test_result_flag: + assert test_result_flag, "Recursive Get Messages Failed" + + +def delete_msg(*postresponse): + """Post DELETE message & verifies that a subsequent GET returns 404.""" + test_result_flag = False + headers = str(postresponse[0]) + headers = headers.replace("'", '"') + headers = json.loads(headers) + location = headers['location'] + url = functionlib.create_url_from_appender(location) + header = functionlib.create_marconi_headers() + deletemsg = http.delete(url, header) + if deletemsg.status_code == 204: + test_result_flag = functionlib.verify_delete(url, header) + else: + print("DELETE message failed") + print("URL") + print url + print("headers") + print header + print("Response Body") + print deletemsg.text + assert test_result_flag, "DELETE Code {}".format(deletemsg.status_code) diff --git a/marconi/tests/system/messages/test_data.csv b/marconi/tests/system/messages/test_data.csv new file mode 100755 index 000000000..75a22b853 --- /dev/null +++ b/marconi/tests/system/messages/test_data.csv @@ -0,0 +1,9 @@ +TestID|httpverb|url|header|body|params|expectedRC|expectedResponseBody +0|PUT |/queues/msgtestqueue||{"messages":{"ttl": 86400}}||201| +1|POST |/queues/msgtestqueue/messages||||201| +2|POST |/queues/msgtestqueue/messages||||201| +3|GET |/queues/msgtestqueue/messages?echo=true||||200| +4|GET |/queues/msgtestqueue/messages?limit=5&echo=true||||200| +5|GET |/queues/msgtestqueue/messages?limit=5&echo=true||||200| +6|POST |/queues/msgtestqueue/messages||{"messages":{"ttl": 86400}}||201| +0|DELETE |/queues/msgtestqueue||||204| \ No newline at end of file diff --git a/marconi/tests/system/queue/__init__.py b/marconi/tests/system/queue/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/marconi/tests/system/queue/getdata.py b/marconi/tests/system/queue/getdata.py new file mode 100755 index 000000000..ff33c59ee --- /dev/null +++ b/marconi/tests/system/queue/getdata.py @@ -0,0 +1,38 @@ +# Copyright (c) 2013 Rackspace, 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 csv + +from marconi.tests.system.common import config +from marconi.tests.system.common import functionlib + + +cfg = config.Config() + + +def get_data(): + """Gets Test Data from a csv file.""" + DATA = [] + with open('marconi/tests/system/queue/test_data.csv', 'rb') as datafile: + test_data = csv.DictReader(datafile, delimiter='|') + for row in test_data: + DATA.append(row) + for row in DATA: + row['header'] = functionlib.get_headers(row['header']) + row['url'] = row['url'].replace("", cfg.base_url) + return DATA + + +API_TEST_DATA = get_data() diff --git a/marconi/tests/system/queue/queue_tests.txt b/marconi/tests/system/queue/queue_tests.txt new file mode 100755 index 000000000..224748e85 --- /dev/null +++ b/marconi/tests/system/queue/queue_tests.txt @@ -0,0 +1,106 @@ +| *Setting* | *Value* | +| Documentation | Marconi - Queue Test Suite | +| Library | ../common/http.py | +| Library | queuefnlib.py | +| Library | ../common/functionlib.py | +| Library | Collections | +| Variables | getdata.py | +| Force Tags | QUEUE | + +| *Test Case* | *Action* | *Argument* | *Argument* | +| 1: PUT QUEUE | [DOCUMENTATION] | Creates, gets & verifies | | +| | ... | Queue | | +| | @{putresponse}= | executetests | ${API_TEST_DATA[0]} | +| | ${url}= | geturlfromlocation | ${putresponse[0]} | +| | ${getresponse}= | executetests | ${API_TEST_DATA[1]} | +| | verifymetadata | ${API_TEST_DATA[0]["body"]} | ${getresponse} | +| 2: PUT QUEUE | [DOCUMENTATION] | Verifies that queue name | | +| | ... | are NOT case sensitive | | +| | ${putresponse}= | executetests | ${API_TEST_DATA[2]} | +| | ${getresponse}= | executetests | ${API_TEST_DATA[3]} | +| | verifymetadata | ${API_TEST_DATA[2]["body"]} | ${getresponse} | +| 3: UPDATE QUEUE | [DOCUMENTATION] | Updates an existing queue | | +| | ${putresponse}= | executetests | ${API_TEST_DATA[4]} | +| | ${getresponse}= | executetests | ${API_TEST_DATA[5]} | +| | verifymetadata | ${API_TEST_DATA[4]["body"]} | ${getresponse} | +| 4: PUT QUEUE | [DOCUMENTATION] | Create Queue with no request | | +| | ... | body | | +| | executetests | ${API_TEST_DATA[6]} | | +| 5: PUT QUEUE | [DOCUMENTATION] | Create Queue with invalid | | +| | ... | Authtoken | | +| | ${header}= | invalidauthtokenheader | | +| | Set To Dictionary | ${API_TEST_DATA[7]} | header | ${header} | # test Setup -Set the POST header +| | executetests | ${API_TEST_DATA[7]} | | +| 6: PUT QUEUE | [DOCUMENTATION] | Create Queue with missing | | +| | ... | header field USERAGENT | | +| | ${header}= | missingheaderfields | | +| | Set To Dictionary | ${API_TEST_DATA[8]} | header | ${header} | # test Setup -Set the POST header +| | executetests | ${API_TEST_DATA[8]} | | +| 7: PUT QUEUE | [DOCUMENTATION] | Verifies metadata toplevel | | +| | ... | field do not start with _ | | +| | executetests | ${API_TEST_DATA[9]} | | +| 8: PUT QUEUE | [DOCUMENTATION] | Header has Accept value that | | +| | ... | is not "application/json" | | +| | ${header}= | plaintextinheader | | +| | Set To Dictionary | ${API_TEST_DATA[10]} | header | ${header} | # test Setup -Set the POST header +| | executetests | ${API_TEST_DATA[10]} | | +| 9: PUT QUEUE | [DOCUMENTATION] | Header has Accept value that | | +| | ... | is "\*/\*" | | +| | ${header}= | asteriskinheader | | +| | Set To Dictionary | ${API_TEST_DATA[11]} | header | ${header} | # test Setup -Set the POST header +| | executetests | ${API_TEST_DATA[11]} | | +| 10: PUT QUEUE | [DOCUMENTATION] | Create queue with Non ASCII | | +| | ... | characters in name | | +| | executetests | ${API_TEST_DATA[12]} | | +| 11: PUT QUEUE | [DOCUMENTATION] | Create queue with Non ASCII | | +| | ... | characters in body | | +| | executetests | ${API_TEST_DATA[13]} | | +| 12: PUT QUEUE | [DOCUMENTATION] | Create queue with metadata | | +| | ... | size = 4KB | | +| | ${reqdata}= | Create Dictionary | metadatasize | ${4096} | +| | ${body}= | getcustombody | ${reqdata} | +| | Set To Dictionary | ${API_TEST_DATA[14]} | body | ${body} | # test Setup -Set the POST body +| | executetests | ${API_TEST_DATA[14]} | | +| 13: PUT QUEUE | [DOCUMENTATION] | Create queue with metadata | | +| | ... | size = 4KB + 1 | | +| | ${reqdata}= | Create Dictionary | metadatasize | ${4097} | +| | ${body}= | getcustombody | ${reqdata} | +| | Set To Dictionary | ${API_TEST_DATA[15]} | body | ${body} | # test Setup -Set the POST body +| | executetests | ${API_TEST_DATA[15]} | | +| 14: PUT QUEUE | [DOCUMENTATION] | Create queue with metadata | | +| | ... | size = 4KB - 1 | | +| | ${reqdata}= | Create Dictionary | metadatasize | ${4095} | +| | ${body}= | getcustombody | ${reqdata} | +| | Set To Dictionary | ${API_TEST_DATA[16]} | body | ${body} | # test Setup -Set the POST body +| | executetests | ${API_TEST_DATA[16]} | | +| 15: PUT QUEUE | [DOCUMENTATION] | Create queue with name | | +| | ... | longer than 64 char | | +| | ${url}= | getqueuename | | +| | Set To Dictionary | ${API_TEST_DATA[17]} | url | ${url} | # test Setup -Set the PUT url +| | executetests | ${API_TEST_DATA[17]} | | +| 16: GET QUEUE STATS | [DOCUMENTATION] | Get Queue Stats | | +| | @{getresponse}= | executetests | ${API_TEST_DATA[18]} | +| | verifyqueuestats | @{getresponse} | | +| 17: LIST QUEUES | [DOCUMENTATION] | List queues with no params | | +| | @{listqueues}= | executetests | ${API_TEST_DATA[19]} | +| | verifylistqueues | @{listqueues} | | +| 18: LIST QUEUES DETAILED | [DOCUMENTATION] | List queues ?detailed=true | | +| | @{listqueues}= | executetests | ${API_TEST_DATA[20]} | +| | verifylistqueues | @{listqueues} | | +| 19: DELETE QUEUE | [DOCUMENTATION] | Delete a queue | | +| | executetests | ${API_TEST_DATA[21]} | | +| 20: PUT QUEUE | [DOCUMENTATION] | Creates a queue with same | | +| | ... | name as deleted | | +| | @{putresponse}= | executetests | ${API_TEST_DATA[22]} | +| | ${url}= | geturlfromlocation | ${putresponse[0]} | +| | ${getresponse}= | executetests | ${API_TEST_DATA[23]} | +| | verifymetadata | ${API_TEST_DATA[22]["body"]} | ${getresponse} | +| 21: PUT QUEUE | [DOCUMENTATION] | Create Queue with invalid | | +| | ... | char in metadata | | +| | executetests | ${API_TEST_DATA[24]} | | +| 22: DELETE QUEUE | [DOCUMENTATION] | Delete a queue | | +| | executetests | ${API_TEST_DATA[25]} | | +| 23: GET QUEUE - 404 | [DOCUMENTATION] | Get non existing queue | | +| | executetests | ${API_TEST_DATA[26]} | | + + diff --git a/marconi/tests/system/queue/queuefnlib.py b/marconi/tests/system/queue/queuefnlib.py new file mode 100644 index 000000000..fd70bac5e --- /dev/null +++ b/marconi/tests/system/queue/queuefnlib.py @@ -0,0 +1,110 @@ +# Copyright (c) 2013 Rackspace, 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 binascii +import json +import os + +from marconi.tests.system.common import functionlib + + +def verify_queue_stats(*get_response): + """Verifies GET queue/stats response. + + Verification Steps: + 1. stats json body has the keys - action & messages. + 2. messages json has the keys - claimed & free. + 3. claimed & free key values are int. + """ + + test_result_flag = True + headers = get_response[0] + body = json.loads(get_response[1]) + + keys_in_body = body.keys() + keys_in_body.sort() + + if (keys_in_body == ["actions", "messages"]): + stats = body["messages"] + keys_in_stats = stats.keys() + keys_in_stats.sort() + if (keys_in_stats == ["claimed", "free"]): + try: + int(stats["claimed"]) + int(stats["free"]) + except Exception: + test_result_flag = False + else: + test_result_flag = False + else: + test_result_flag = False + + if test_result_flag: + return test_result_flag + else: + print headers + print body + assert test_result_flag, "Get Request stats failed" + + +def get_queue_name(namelength=65): + """Returns a queuename of specified length. + + By default, a name longer than Marconi allows - currently 64 char. + """ + + appender = "/queues/" + binascii.b2a_hex(os.urandom(namelength)) + url = functionlib.create_url_from_appender(appender) + return url + + +def verify_list_queues(*list_queue_response): + """Verifies the response of list queues.""" + response_body = json.loads(list_queue_response[1]) + + links = response_body['links'] + href = links[0]['href'] + detail_enabled = 'detailed=true' in href + + queue_list = response_body['queues'] + test_result_flags = [verify_listed(queue, detail_enabled) + for queue in queue_list] + + if False in test_result_flags: + test_result_flag = False + print 'List Queue API response: {}'.format(response_body) + assert test_result_flag, 'List Queue failed' + + +def verify_listed(queue, detail_enabled): + '''Verifies the listed queues.''' + test_result_flag = True + + keys = queue.keys() + keys.sort() + + if detail_enabled: + expected_keys = ['href', 'metadata', 'name'] + else: + expected_keys = ['href', 'name'] + + if keys == expected_keys: + return test_result_flag + else: + print 'list_queue response does not match expected response' + print queue + test_result_flag = False + + return test_result_flag diff --git a/marconi/tests/system/queue/test_data.csv b/marconi/tests/system/queue/test_data.csv new file mode 100755 index 000000000..ed6a8fd70 --- /dev/null +++ b/marconi/tests/system/queue/test_data.csv @@ -0,0 +1,28 @@ +TestID|httpverb|url|header|body|params|expectedRC|expectedResponseBody +1|PUT |/queues/qtestqueue||{"messages":{"ttl": 86400}}||201| +1|GET |/queues/qtestqueue||||200|{"ttl": 86400} +2|PUT |/queues/qtestqueue||{"messages": {"ttl": 86400}}||204| +2|GET |/queues/qtestqueue||||200|{"ttl": 86400} +3|PUT |/queues/qtestqueue||{"messages": {"ttl": 86400}}||204| +3|GET |/queues/qtestqueue||||200|{"ttl": 86400} +4|PUT |/queues/qtestqueue||||400|{"title": "Bad request","description": "Missing queue metadata."} +5|PUT |/queues/qtestqueue||{"messages": {"ttl": 86400}}||401| +6|PUT |/queues/qtestqueue||{"messages": {"ttl": 86400}}||400| +7|PUT |/queues/qtestqueue||{"_TOPLEVEL": {"ttl": 86400}}||400| +8|PUT |/queues/qtestqueue||{"TOPLEVEL": {"ttl": 86400}}||406| +9|PUT |/queues/qtestqueue||{"TOPLEVEL": {"ttl": 86400}}||200| +10|PUT |/queues/汉字/漢字||{"messages": {"ttl": 86400}}||201| +11|PUT |/queues/qtestqueue||{"汉字": {"ttl": 86400}}||201| +12|PUT |/queues/qtestqueue||||201| +13|PUT |/queues/qtestqueue||||400| +14|PUT |/queues/qtestqueue||||204| +15|PUT |||{"messages":{"ttl": 86400}}||400| +16|GET |/queues/qtestqueue/stats||||200| +17|GET |/queues||||200| +18|GET |/queues?detailed=true||||200| +19|DELETE |/queues/qtestqueue||||204| +20|PUT |/queues/qtestqueue||{"messages":{"ttl": 86400}}||201| +20|GET |/queues/qtestqueue||||200|{"ttl": 86400} +21|PUT |/queues/qtestqueue||{"mess~&%^":{"ttl": 86400}}||400| +22|DELETE |/queues/qtestqueue||||204| +23|GET |/queues/nonexistingqueue||||404| \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 64ee6708a..0d35a0930 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [nosetests] where=marconi/tests verbosity=2 +exclude=system/* with-doctest = true diff --git a/tools/system-test-requires b/tools/system-test-requires new file mode 100644 index 000000000..b140c0d27 --- /dev/null +++ b/tools/system-test-requires @@ -0,0 +1,2 @@ +robotframework +requests \ No newline at end of file