diff --git a/JumpCloud QA Assignment/FurtherTestings.txt b/JumpCloud QA Assignment/FurtherTestings.txt new file mode 100644 index 0000000..9d4b46f --- /dev/null +++ b/JumpCloud QA Assignment/FurtherTestings.txt @@ -0,0 +1,11 @@ +As far as testing goes, I was not able to get into much of load/performance testing. +At no point did I issue a 100 or a 1000 requests and see how the application handled +that many requests. Furthermore, I did not do much testing in terms of headers and parameters in +the hash endpoint or headers in the stats endpoint and would have like to have dived more +into this area had time provided. In addition, I would have liked to have asked and tested if +there were any validations around passwords, such as does a password have to contain 1 uppercase character, +1 special character, and must be at least 8 characters long and see if the endpoints validated against that. +Moreover, I would have liked to have tested passing in multiple keys in the POST response to +the hash endpoint. I did testing around the password key, but not adding a key in addition to that one. Lastly, I would +have like to have tested the actual error messages a bit more and see where they could have been improved from +malformed input and Method Not Supported. diff --git a/JumpCloud QA Assignment/HTTPSession.py b/JumpCloud QA Assignment/HTTPSession.py new file mode 100644 index 0000000..d9f7f1e --- /dev/null +++ b/JumpCloud QA Assignment/HTTPSession.py @@ -0,0 +1,29 @@ +import requests + +class HTTPSession(object): + """ + Class that holds the requests session for all api calls. + """ + + def __init__(self): + requests.packages.urllib3.disable_warnings() + self.rs = requests.Session() + self.baseurl = 'http://radiant-gorge-83016.herokuapp.com' + + def print_res_info(self, res): + """ + Function to print useful info when something goes wrong. + + Prints: + -URL that was hit. + -Response body. + -HTTP response code. + + Args: + res (obj): Response object from a request. + + Returns: + """ + print('INFO: USED URL: {url}'.format(url=res.url)) + print('INFO: RESPONSE BODY: {res_body}'.format(res_body=res.text)) + print('INFO: RESPONSE STATUS: {status_code}'.format(status_code=res.status_code)) diff --git a/JumpCloud QA Assignment/HTTPSession.pyc b/JumpCloud QA Assignment/HTTPSession.pyc new file mode 100644 index 0000000..cbce757 Binary files /dev/null and b/JumpCloud QA Assignment/HTTPSession.pyc differ diff --git a/JumpCloud QA Assignment/PasswordHashingTest.py b/JumpCloud QA Assignment/PasswordHashingTest.py new file mode 100644 index 0000000..6cb38ed --- /dev/null +++ b/JumpCloud QA Assignment/PasswordHashingTest.py @@ -0,0 +1,737 @@ +from HTTPSession import HTTPSession +from multiprocessing import Pool, Process, Queue +import json +import hashlib +import base64 +import time + +def post_hash(password, queue=None): + """ + Function to submit POST request to hash endpoint. + + Args: + password (str): Password to be hashed and ecoded. + queue (obj): Multiprocessing queue object to hold response object + + Returns: + res (obj): Response object from POST request + """ + + http_session = HTTPSession() + endpoint = '/hash' + params = { + 'password' : password + } + + res = http_session.rs.post(http_session.baseurl + endpoint, json=params) + + if queue is not None: + queue.put(res) + else: + return res + +def get_hash(job_id): + """ + Function to submit GET request to hash endpoint. + + Args: + job_id (str): Job ID that links POST request to encoded password hash. + + Returns: + res (obj): Response object from GET request + """ + + time.sleep(7) + + http_session = HTTPSession() + endpoint = '/hash' + + res = http_session.rs.get(http_session.baseurl + endpoint + '/' + job_id) + + return res + +def get_stats(): + """ + Function to submit GET request to stats endpoint. + + Args: + + Returns: + res (obj): Response object from GET request + """ + + http_session = HTTPSession() + endpoint = '/stats' + + res = http_session.rs.get(http_session.baseurl + endpoint) + + return res + +def shutdown(queue=None): + """ + Function to submit Shutdown request to hash endpoint. + + Args: + queue (obj): Multiprocessing queue object to hold response object + + Returns: + res (obj): Response object from POST request + """ + + http_session = HTTPSession() + endpoint = '/hash' + data = 'shutdown' + + res = http_session.rs.post(http_session.baseurl + endpoint, data=data) + + if queue is not None: + queue.put(res) + else: + return res + +def is_json(res): + """ + Function to validate JSON for response object + + Args: + res (obj): Response object from a request. + + Returns: + bool: Returns True if response is valid JSON, else returns False. + """ + try: + json.loads(res.text) + except ValueError as error: + return False + return True + +def is_key(res, key): + """ + Function to validate key in JSON for response object + + Args: + res (obj): Response object from a request. + key (str): Key to check for in response + + Returns: + bool: Returns True if key exists, else returns False. + """ + if is_json(res): + json_contents = json.loads(res.text) + if key in json_contents: + return True + else: + return False + else: + return False + +def is_shutdown_over(): + """ + Function to validate shutdown is over + + Args: + + Returns: + bool: Returns True once shutdown is over. + """ + shutdown_over = False + while(not shutdown_over): + res = get_stats() + if (res.status_code < 300 and res.status_code >=200): + shutdown_over = True + else: + time.sleep(300) + return shutdown_over + +def test_is_post_hash_successful(password): + """ + Test to check that POST request to hash endpoint is successful + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + res = post_hash(password) + + if (res.status_code >= 300 or res.status_code < 200): + print 'FAIL: test_is_post_hash_successful' + print 'Expected Status Code: 2xx' + print 'Actual Status Code: ' + str(res.status_code) + else: + print 'PASS: test_is_post_hash_successful' + +def test_is_get_hash_successful(password): + """ + Test to check that GET request to hash endpoint is successful + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + post_res = post_hash(password) + get_res = get_hash(post_res.text) + + if (get_res.status_code >= 300 or get_res.status_code < 200): + print 'FAIL: test_is_get_hash_successful' + print 'Expected Status Code: 2xx' + print 'Actual Error Code: ' + str(get_res.status_code) + else: + print 'PASS: test_is_get_hash_successful' + +def test_is_get_stats_successful(): + """ + Test to check that GET request to stats endpoint is successful + + Args: + + Returns: + """ + + res = get_stats() + + if (res.status_code >= 300 or res.status_code < 200): + print 'FAIL: test_is_get_stats_successful' + print 'Expected Status Code: 2xx' + print 'Actual Error Code: ' + str(res.status_code) + else: + print 'PASS: test_is_get_stats_successful' + +def test_is_shutdown_successful(): + """ + Test to check that shutting down is successful + + Args: + + Returns: + """ + + res = shutdown() + + if (res.status_code >= 300 or res.status_code < 200): + print 'FAIL: test_is_shutdown_successful' + print 'Expected Status Code: 2xx' + print 'Actual Status Code: ' + str(res.status_code) + else: + print 'PASS: test_is_shutdown_successful' + +def test_is_job_identifier_returned(password): + """ + Test to check that job identifier is returned when submitting POST request to hash endpoint + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + res = post_hash(password) + + if not res.text: + print 'FAIL: test_is_job_identifier_returned' + print 'Job identifier is expected to be returned' + print 'No job identifier was returned' + else: + print 'PASS: test_is_job_identifier_returned' + +def test_is_job_identifier_returned_immediately(password): + """ + Test to check that job identifier is returned immediately when submitting POST request to hash endpoint + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + start = time.time() + res = post_hash(password) + round_trip = time.time() - start + + if round_trip > 2: + print 'FAIL: test_is_job_identifier_returned_immediately' + print 'Job identifier not returned immediately' + print 'Job identifier took ' + str(round_trip) + ' seconds to return' + print 'Job identifier expected to return in less than 2 seconds' + else: + print 'PASS: test_is_job_identifier_returned_immediately' + +def test_is_get_hash_without_job_identifier_allowed(): + """ + Test to check that GET request to hash endpoint without a job identifier is not allowed + + Args: + + Returns: + """ + + http_session = HTTPSession() + endpoint = '/hash' + + res = http_session.rs.get(http_session.baseurl + endpoint) + + if res.status_code >= 400: + print 'PASS: test_is_get_hash_without_job_identifier_allowed' + else: + print 'FAIL: test_is_get_hash_without_job_identifier_allowed' + print 'GET request to hash endpoint without a job identifier should not be allowed' + +def test_is_password_encoded(password): + """ + Test to check that password is hashed using SHA512 hashing algorithm and base64 econded. + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + post_res = post_hash(password) + get_res = get_hash(post_res.text) + + expected_encode = base64.b64encode(hashlib.sha512(password).hexdigest()) + + if get_res.text != expected_encode: + print 'FAIL: test_is_password_encoded' + print 'Expected Ecodeed Value: ' + expected_encode + print 'Actual Encoded Value: ' + get_res.text + else: + print 'PASS: test_is_password_encoded' + +def test_is_empty_string_allowed_as_password(password): + """ + Test to check that an empty string cannot be passed as a password + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + res = post_hash(password) + + if (res.status_code < 300 and res.status_code >= 200): + print 'FAIL: test_is_empty_string_allowed_as_password' + print 'Empty string should not be allowed as a password' + else: + print 'PASS: test_is_empty_string_allowed_as_password' + +def test_is_malformed_input_post_hash_allowed(): + """ + Test to check that malformed JSON cannot be passed in the POST request to hash endpoint + + Args: + + Returns: + """ + + http_session = HTTPSession() + endpoint = '/hash' + params = 'password' + + res = http_session.rs.post(http_session.baseurl + endpoint, json=params) + + if res.status_code >= 400: + print 'PASS: test_is_malformed_input_post_hash_allowed' + else: + print 'FAIL: test_is_malformed_input_post_hash_allowed' + print 'Malformed JSON should not be allowed in POST request to hash endpoint' + +def test_is_different_key_post_hash_allowed(password): + """ + Test to check that a key other than password cannot be passed in POST request to hash endpoint + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + http_session = HTTPSession() + endpoint = '/hash' + params = { + 'p' : password + } + + res = http_session.rs.post(http_session.baseurl + endpoint, json=params) + + if res.status_code >= 400: + print 'PASS: test_is_different_key_post_hash_allowed' + else: + print 'FAIL: test_is_different_key_post_hash_allowed' + print 'password should be the only key allowed in POST request to hash endpoint' + print 'Passed Key: p' + +def test_is_passing_empty_json_allowed(): + """ + Test to check that empty JSON cannot be passed in POST request to hash endpoint + + Args: + + Returns: + """ + + http_session = HTTPSession() + endpoint = '/hash' + params = {} + + res = http_session.rs.post(http_session.baseurl + endpoint, json=params) + + if res.status_code >= 400: + print 'PASS: test_is_passing_empty_json_allowed' + else: + print 'FAIL: test_is_passing_empty_json_allowed' + print 'Empty JSON should not be allowed to be passed to the hash endpoint' + +def test_is_stats_response_json(): + """ + Test to check that GET request to stats endpoint returns JSON response + + Args: + + Returns: + """ + + res = get_stats() + if not is_json(res): + print 'FAIL: test_is_stats_response_json' + print 'Expected Response Should be valid JSON' + print 'Actual Response: ' + res.text + else: + print 'PASS: test_is_stats_response_json' + +def test_is_TotalRequests_key(): + """ + Test to check that TotalRequests is a Key in JSON Response from stats endpoint + + Args: + + Returns: + """ + + key = 'TotalRequests' + res = get_stats() + + if not is_key(res, key): + print 'FAIL: test_is_TotalRequests_key' + print key + ' should be Key in JSON response' + print 'Actual Response: ' + res.text + else: + print 'PASS: test_is_TotalRequests_key' + +def test_is_AverageTime_key(): + """ + Test to check that AverageTime is a Key in JSON Response from stats endpoint + + Args: + + Returns: + """ + + key = 'AverageTime' + res = get_stats() + + if not is_key(res, key): + print 'FAIL: test_is_AverageTime_key' + print key + ' should be Key in JSON response' + print 'Actual Response: ' + res.text + else: + print 'PASS: test_is_AverageTime_key' + +def test_is_post_stats_allowed(): + """ + Test to check that the stats endpoint does not support POST + + Args: + + Returns: + """ + + http_session = HTTPSession() + endpoint = '/stats' + + res = http_session.rs.post(http_session.baseurl + endpoint) + + if res.status_code >= 400: + print 'PASS: test_is_post_stats_allowed' + else: + print 'FAIL: test_is_post_stats_allowed' + print 'The stats endpoint should not support POST requests' + +def test_is_params_in_get_stats_request_allowed(): + """ + Test to check that the stats endpoint does not support parameters on GET request + + Args: + + Returns: + """ + + http_session = HTTPSession() + endpoint = '/stats' + + res = http_session.rs.get(http_session.baseurl + endpoint + '?name1=value1&name2=value2') + + if res.status_code >= 400: + print 'PASS: test_is_params_in_get_stats_request_allowed' + else: + print 'FAIL: test_is_params_in_get_stats_request_allowed' + print 'The stats endpoint should not support parameters in URL in GET request' + +def test_is_TotalRequests_incremented_successfully(password): + """ + Test to check that TotalRequests is incremented successfully + + Args: + password (str): Password to be hashed and ecoded. + + Returns: + """ + + res = get_stats() + data = res.json() + preincrement_TotalRequests = data['TotalRequests'] + + expected_TotalRequests = data['TotalRequests'] + 1 + + post_hash(password) + res = get_stats() + data = res.json() + actual_TotalRequests = data['TotalRequests'] + + if actual_TotalRequests == expected_TotalRequests: + print 'PASS: test_is_TotalRequests_incremented_successfully' + else: + print 'FAIL: test_is_AverageTime_calculated_correctly' + print 'Expected Value: ' + str(expected_TotalRequests) + print 'Actual Value: ' + str(actual_TotalRequests) + +def test_is_simultaneous_post_hash_successful(passwords): + """ + Test to check that simultaneous POST requests are supported for hash endpoint. + + Args: + passwords (str list): Passwords to be hashed and ecoded. + + Returns: + """ + success = True + processes = Pool(processes=10) + responses = processes.map_async(post_hash, passwords).get(999999) + for res in responses: + if (res.status_code >= 300 or res.status_code < 200): + success = False + break + + if not success: + print 'FAIL: test_is_simultaneous_post_hash_successful' + print 'Expected Status Code form: 2xx' + print 'At least one process had a status code that was not of the form 2xx' + else: + print 'PASS: test_is_simultaneous_post_hash_successful' + +def test_is_simultaneous_get_hash_successful(passwords): + """ + Test to check that simultaneous GET requests are supported for hash endpoint. + + Args: + passwords (str list): Passwords to be hashed and ecoded. + + Returns: + """ + + job_ids = [] + for password in passwords: + post_res = post_hash(password) + job_ids.append(post_res.text) + + success = True + processes = Pool(processes=10) + responses = processes.map_async(get_hash, job_ids).get(999999) + + for res in responses: + if (res.status_code >= 300 or res.status_code < 200): + success = False + break + + if not success: + print 'FAIL: test_is_simultaneous_get_hash_successful' + print 'Expected Status Code form: 2xx' + print 'At least one process had a status code that was not of the form 2xx' + else: + print 'PASS: test_is_simultaneous_get_hash_successful' + +def test_is_remaining_password_hashing_allowed_to_complete(password): + """ + Test to check that remaining password hashings are allowed to complete. + + Args: + passwords (str): Password to be hashed and ecoded. + + Returns: + """ + + shutdown_over = False + while (not shutdown_over): + shutdown_over = is_shutdown_over() + + queue = Queue() + p1 = Process(target=post_hash, args=(password,queue,)) + p1.start() + p2 = Process(target=shutdown, args=(queue,)) + p2.start() + + shutdown_res = queue.get() + + if (shutdown_res.status_code >= 300 or shutdown_res.status_code < 200 or shutdown_res.text): + print 'FAIL: test_is_remaining_password_hashing_allowed_to_complete' + print 'Shutdown did not succeed or shutdown response did not come first' + print 'Shutdown Response: ' + shutdown_res.text + else: + post_res = queue.get() + if (post_res.status_code >= 300 or post_res.status_code < 200 or not post_res.text): + print 'FAIL: test_is_remaining_password_hashing_allowed_to_complete' + print 'POST to Hash did not succeed or response was empty' + print 'POST Response: ' + post_res.text + else: + print 'PASS: test_is_remaining_password_hashing_allowed_to_complete' + +def test_is_new_requests_rejected_during_shutdown(password): + """ + Test to check that new requests are rejected during shutdown. + + Args: + passwords (str): Password to be hashed and ecoded. + + Returns: + """ + + shutdown_over = False + while (not shutdown_over): + shutdown_over = is_shutdown_over() + + queue = Queue() + + p1 = Process(target=shutdown, args=(queue,)) + p1.start() + + p2 = Process(target=post_hash, args=(password,queue,)) + + shutdown_res = queue.get() + + p2.start() + + if (shutdown_res.status_code >= 300 or shutdown_res.status_code < 200 or shutdown_res.text): + print 'FAIL: test_is_new_requests_rejected_during_shutdown' + print 'Shutdown did not succeed' + print 'Shutdown Response: ' + shutdown_res.text + else: + post_res = queue.get() + if (post_res.status_code < 300 and post_res.status_code >= 200): + print 'FAIL: test_is_new_requests_rejected_during_shutdown' + print 'POST to Hash succeeded' + print 'POST Response: ' + post_res.text + else: + print 'PASS: test_is_new_requests_rejected_during_shutdown' + +def test_is_AverageTime_calculated_correctly(passwords): + """ + Test to check that AverageTime is calculated correctly. + + Args: + passwords (str list): Passwords to be hashed and ecoded. + + Returns: + """ + + shutdown() + + shutdown_over = False + while (not shutdown_over): + shutdown_over = is_shutdown_over() + + total_time = 0 + for password in passwords: + start = time.time() + res = post_hash(password) + round_trip = time.time() - start + total_time = total_time + round_trip + + expected_AverageTime = str(int((total_time / len(passwords)) * 1000))[:5] + + res = get_stats() + data = res.json() + + actual_AverageTime = data['AverageTime'] + + if actual_AverageTime == expected_AverageTime: + print 'PASS: test_is_AverageTime_calculated_correctly' + else: + print 'FAIL: test_is_AverageTime_calculated_correctly' + print 'Expected Value: ' + str(expected_AverageTime) + print 'Actual Value: ' + str(actual_AverageTime) + +def test_is_get_hash_with_non_existent_job_identifier_allowed(): + """ + Test to check that GET request hash endpoint with a non existent job identifier is not supported. + + Args: + + Returns: + """ + + shutdown() + + shutdown_over = False + while (not shutdown_over): + shutdown_over = is_shutdown_over() + + non_existent_job_identifier = '999' + res = get_hash(non_existent_job_identifier) + + if res.status_code >= 400: + print 'PASS: test_is_get_hash_with_non_existent_job_identifier_allowed' + else: + print 'FAIL: test_is_get_hash_with_non_existent_job_identifier_allowed' + print 'The hash endpoint should not support GET request with non existent job identifier' + +def main(): + """ + Run through all tests for http://radiant-gorge-83016.herokuapp.com + + Returns: + """ + + password = 'angrymonkey' + passwords = ['angry', 'monkey', 'almond', 'ham', 'sam', 'MILK','hfhf123', 'Ll', '1245', 'aB123'] + empty_string = '' + + test_is_post_hash_successful(password) + test_is_get_hash_successful(password) + test_is_get_stats_successful() + test_is_job_identifier_returned(password) + test_is_job_identifier_returned_immediately(password) + test_is_get_hash_without_job_identifier_allowed() + test_is_password_encoded(password) + test_is_empty_string_allowed_as_password(empty_string) + test_is_malformed_input_post_hash_allowed() + test_is_different_key_post_hash_allowed(password) + test_is_passing_empty_json_allowed() + test_is_stats_response_json() + test_is_TotalRequests_key() + test_is_AverageTime_key() + test_is_post_stats_allowed() + test_is_params_in_get_stats_request_allowed() + test_is_TotalRequests_incremented_successfully(password) + test_is_simultaneous_post_hash_successful(passwords) + test_is_simultaneous_get_hash_successful(passwords) + test_is_shutdown_successful() + test_is_remaining_password_hashing_allowed_to_complete(password) + test_is_new_requests_rejected_during_shutdown(password) + test_is_AverageTime_calculated_correctly(passwords) + test_is_get_hash_with_non_existent_job_identifier_allowed() + +if __name__ == '__main__': + main() diff --git a/JumpCloud QA Assignment/TestPlan.txt b/JumpCloud QA Assignment/TestPlan.txt new file mode 100644 index 0000000..4e42e97 --- /dev/null +++ b/JumpCloud QA Assignment/TestPlan.txt @@ -0,0 +1,372 @@ +Test Plan for http://radiant-gorge-83016.herokuapp.com/ +USEFUL LINKS: +- HTTP Status Codes +-- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status +-- http://www.restapitutorial.com/httpstatuscodes.html +- HTTP Parameters +-- https://www.w3schools.com/tags/ref_httpmethods.asp +- Curl Multiple Requests +-- https://stackoverflow.com/questions/3110444/how-to-run-multiple-curl-requests-processed-sequentially + +TITLE: +- POST Request to Hash Endpoint to Hash Password is Successful +INTENT: +- Make sure POST request to hash endpoint to hash password returns a successful HTTP status code +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following JSON body +---- {"password": ""} +VALIDATION: +- HTTP status code that is returned is of the form 2xx + +TITLE: +- GET Request to Hash Endpoint is Successful +INTENT: +- Make sure GET request to hash endpoint returns a successful HTTP status code +SETUP: +- Need to have submitted a password for hashing and a corresponding job id +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/hash/ +VALIDATION: +- HTTP status code that is returned is of the form 2xx + +TITLE: +- GET Request to Stats Endpoint is Successful +INTENT: +- Make sure GET request to stats endpoint returns a successful HTTP status code +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/stats +VALIDATION: +- HTTP status code that is returned is of the form 2xx + +TITLE: +- POST Request to Hash Endpoint to Shutdown is Successful +INTENT: +- Make sure POST request to hash endpoint to shutdown returns a successful HTTP status code +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following data +---- shutdown +VALIDATION: +- HTTP status code that is returned is of the form 2xx + +TITLE: +- Job Identifier Returned After Submitting POST Request to hash endpoint +INTENT: +- Make sure that a job identifier is returned after POST request to hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following JSON body +---- {"password": ""} +VALIDATION: +- Job identifier is returned in response + +TITLE: +- Job Identifier Returned Immediately After Submitting POST request to hash endpoint +INTENT: +- Make sure that the job identifier is returned quickly after submitting POST request +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following JSON body +---- {"password": ""} +VALIDATION: +- Job identifier is returned in no longer than two seconds +TO DO: Define what is meant by immediately + +TITLE: +- GET Request to Hash Endpoint Should not Succeed Without a Job identifier +INTENT: +- Make sure that GET request to hash endpoint fails when a job identifier is not specified +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/hash +VALIDATION +- HTTP status code that is returned is of the form 4xx + +TITLE: +- Password Hash Encoded Correctly +INTENT: +- Make sure that the password hash is encoded correctly +SETUP: +- Have submitted a successful POST request to hash endpoint and know the job identifier +-- Shutdown or Crash has not occurred since then +--- If it has, just submit a successful POST request to hash endpoint and get another job identifier +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/hash/ +--- Get the encoded password hash from the GET request +---- Hash the submitted password using the SHA512 algorithm +----- base64 encode the password hash +------ Compare the encoded password hash from the GET request to the encoded password hash using the SHA512 algorithm and base64 encoder +VALIDATION: +- Encoded password hash from the GET request matches encoded password hash using the SHA512 algorithm and base64 encoder + +TITLE: +- Empty String is not an Acceptable Password in POST request to Hash Endpoint +INTENT: +- Make sure that empty string is not an acceptable password in POST request to hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following JSON body +---- {"password": ""} +VALIDATION: +- HTTP status code that is returned is of the form 4xx +- Error message that says a password must be passed and be non-empty +TO DO: Define what error message should be + +TITLE: +- Malformed JSON is not Allowed to be Passed in POST Request to Hash Endpoint +INTENT: +- Make sure that bad JSON cannot be passed in POST request to hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following body +---- "password" +VALIDATION: +- HTTP status code that is returned is of the form 4xx +- Error message that says malformed input has been passed +TO DO: Define what error message should be + +TITLE: +- Key Other than password Should not be Allowed to be Passed in POST Request to Hash Endpoint +INTENT: +- Make sure that a different key in replace of password cannot be submitted in POST request to hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following body +---- {"password": ""} +VALIDATION: +- HTTP status code that is returned is of the form 4xx +- Error message says incorrect key passed +TO DO: Define what error message should be + +TITLE: +- Passing Empty JSON is not Allowed in POST Request to Hash Endpoint +INTENT: +- Make sure that empty JSON cannot be submitted in POST request to hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following body +---- {} +VALIDATION +- HTTP status code that is returned is of the form 4xx +- Error message says passed data was empty +TO DO: Define Error message + +TITLE: +- Response After Submitting GET Request to Stats Endpoint is JSON +INTENT: +- Make sure that the stats endpoint sends actual JSON in response after submitting GET request +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/stats +VALIDATION: +- Response from stats endpoint is valid JSON + +TITLE: +- Total Requests is in Response After Submitting GET Request to Stats Endpoint +INTENT: +- Make sure that TotalRequests is in response after submitting GET request to stats endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/stats +VALIDATION: +- TotalRequests is in response from stats endpoint + +TITLE: +- Average Time is in Response After Submitting GET request to Stats Endpoint +INTENT: +- Make sure that AverageTime is in response after submitting GET request to stats endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/stats +VALIDATION: +- AverageTime is in response from stats endpoint + +TITLE: +- Stats Endpoint does not Support POST +INTENT: +- Make sure that stats endpoint does not support POST requests +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a POST request +-- http://radiant-gorge-83016.herokuapp.com/stats +VALIDATION: +- HTTP status code that is returned is of the form 4xx +- Error message that says method is not supported +TO DO: Define what error message should be + +TITLE: +- GET Request on Stats Endpoint Should Accept No Data +INTENT: +- Make sure that stats endpoint accepts no data +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/stats?name1=value1&name2=value2 +VALIDATION: +- HTTP status code that is returned is of the form 4xx +- Error message that says parameters are not allowed and/or supported +TO DO: Define error messages +TO DO: Define if this test case is correct +TO DO: Does this include headers as well as parameters? Or just one or the other? + +TITLE: +- Total Requests is Incremented Correctly +INTENT: +- Make sure that TotalRequests is incremented when a POST request is submitted to the hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/stats +--- Observe the value for TotalRequests +---- Hit the following endpoint with a POST request +----- http://radiant-gorge-83016.herokuapp.com/hash +------ Hit the following endpoint with a GET request +------- http://radiant-gorge-83016.herokuapp.com/stats +-------- Observe the value for TotalRequests +VALIDATION: +- The value for TotalRequests has incremented by 1 + +TITLE: +- Application Supports Simultaneous POST Requests to Hash Endpoint +INTENT: +- Make sure that the application supports simultaneous POST requests to the hash endpoint +SETUP: +- No setup required +STEPS: +- Hit the following endpoint multiple times simultaneously with a POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- This can be done using curl +---- Example: curl -X POST -H "application/json" -d '{"password":"angrymonkey"}' http://radiant-gorge-83016.herokuapp.com/hash & curl -X POST -H "application/json" -d '{"password":"angrymonkey"}' http://radiant-gorge-83016.herokuapp.com/hash & curl -X POST -H "application/json" -d '{"password":"angrymonkey"}' http://radiant-gorge-83016.herokuapp.com/hash +VALIDATION: +- All requests have status codes of the form 2xx +- All requests return job identifiers + +TITLE: +- Application Supports Simultaneous GET Requests to Hash Endpoint +INTENT: +- Make sure that the application supports simultaneous GET requests to the hash endpoint +SETUP: +- Have submitted multiple successful POST requests to hash endpoint and know the job identifiers +-- Shutdown or Crash has not occurred since then +--- If it has, just submit multiple successful POST requests to hash endpoint and get several job identifiers +STEPS: +- Hit the following endpoint multiple times simultaneously with a GET request +-- http://radiant-gorge-83016.herokuapp.com/hash/ +--- This can be done using curl +---- curl http://radiant-gorge-83016.herokuapp.com/hash/ & curl http://radiant-gorge-83016.herokuapp.com/hash/ & curl http://radiant-gorge-83016.herokuapp.com/hash/ +VALIDATION: +- All requests have status codes of the form 2xx +- All requests return encoded password hashes + +TITLE: +- Application allows password hashings in progress to complete once shutdown has started +INTENT: +- Make sure that password hashing in progress are allowed to finish once shutdown has started +SETUP: +- No setup required +STEPS: +- Hit the following endpoint POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +---- The request needs to include following body +----- {"password": ""} +------ Hit the following endpoint with POST request simultaneously +------- http://radiant-gorge-83016.herokuapp.com/hash +-------- The request needs to include following data +--------- shutdown +VALIDATION: +- Shutdown returns status code of the form 2xx +- Password hash request returns job identifier +- Password hash request returns status code of the form 2xx + +TITLE: +- Application Denies Any New Requests Once Shutdown has Started +INTENT: +- Make sure that all new requests are denied once shutdown has started +SETUP: +- No setup required +STEPS: +- Hit the following endpoint with POST request +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following data +---- shutdown +----- Hit the following endpoint POST request once the shutdown has successfully started +------ http://radiant-gorge-83016.herokuapp.com/hash +------- The request needs to include following body +-------- {"password": ""} +VALIDATION: +- Shutdown returns status code of the form 2xx +- Password hash returns an error status code either of the form 4xx or 5xx + +TITLE: +- Average Time for Password Hash POST Requests is Calculated Correctly +INTENT: +- Make sure that the AverageTime for password hash POST requests is calculated correctly +SETUP: +- Erase all existing data by submitting a shutdown +STEPS: +- Hit the following endpoint POST request once the shutdown has successfully started +-- http://radiant-gorge-83016.herokuapp.com/hash +--- The request needs to include following body +---- {"password": ""} +----- Time the request +------ Repeat the above steps a few times +------- Calculate the AverageTime by taking the time for all requests combined and dividing it by the number of requests +-------- Compare it to the AverageTime from Stats Endpoint +VALIDATION: +- AverageTime from stats endpoint checks computed AverageTime +TO DO: Check AverageTime Calculation + +TITLE: +- GET Request to Hash Endpoint Should Return Error When Hit With Non Existent Job Identifier +INTENT: +- To make sure that the hash endpoint does not support a GET request when called with non existent job identifier +SETUP: +- Erase all existing data by submitting a shutdown +STEPS: +- Hit the following endpoint with a GET request +-- http://radiant-gorge-83016.herokuapp.com/hash/ +VALIDATION: +- HTTP status code that is returned is of the form 4xx +- Error message is displayed saying job identifier does not exist +TO DO: Define error message diff --git a/JumpCloud QA Assignment/TestResults.txt b/JumpCloud QA Assignment/TestResults.txt new file mode 100644 index 0000000..fcc419a --- /dev/null +++ b/JumpCloud QA Assignment/TestResults.txt @@ -0,0 +1,37 @@ +PASS: test_is_post_hash_successful +PASS: test_is_get_hash_successful +PASS: test_is_get_stats_successful +PASS: test_is_job_identifier_returned +FAIL: test_is_job_identifier_returned_immediately +Job identifier not returned immediately +Job identifier took 5.32026696205 seconds to return +Job identifier expected to return in less than 2 seconds +PASS: test_is_get_hash_without_job_identifier_allowed +FAIL: test_is_password_encoded +Expected Ecodeed Value: MzRkZDBmMDBhYjYyNzlhY2EyNGQ4ZjNmNDFkZTc3MDFlMzMzMWU0NmVmNjQzNzcwNjE4ODgzOWYwYjQzNzZmZmM1MjE2YmRjY2I1YjBhMDliZWVhOGJiMzZlZjEwZjAyNzdmMzJhOGQwN2IyMDg4ZDI5NThhMGM2YTdiZTAwZDY= +Actual Encoded Value: NN0PAKtieayiTY8/Qd53AeMzHkbvZDdwYYiDnwtDdv/FIWvcy1sKCb7qi7Nu8Q8Cd/MqjQeyCI0pWKDGp74A1g== +FAIL: test_is_empty_string_allowed_as_password +Empty string should not be allowed as a password +PASS: test_is_malformed_input_post_hash_allowed +FAIL: test_is_different_key_post_hash_allowed +password should be the only key allowed in POST request to hash endpoint +Passed Key: p +FAIL: test_is_passing_empty_json_allowed +Empty JSON should not be allowed to be passed to the hash endpoint +PASS: test_is_stats_response_json +PASS: test_is_TotalRequests_key +PASS: test_is_AverageTime_key +FAIL: test_is_post_stats_allowed +The stats endpoint should not support POST requests +FAIL: test_is_params_in_get_stats_request_allowed +The stats endpoint should not support parameters in URL in GET request +PASS: test_is_TotalRequests_incremented_successfully +PASS: test_is_simultaneous_post_hash_successful +PASS: test_is_simultaneous_get_hash_successful +PASS: test_is_shutdown_successful +PASS: test_is_remaining_password_hashing_allowed_to_complete +PASS: test_is_new_requests_rejected_during_shutdown +FAIL: test_is_AverageTime_calculated_correctly +Expected Value: 5214 +Actual Value: 101277 +PASS: test_is_get_hash_with_non_existent_job_identifier_allowed diff --git a/JumpCloud QA Assignment/bugs.txt b/JumpCloud QA Assignment/bugs.txt new file mode 100644 index 0000000..681520a --- /dev/null +++ b/JumpCloud QA Assignment/bugs.txt @@ -0,0 +1,90 @@ +Here are the following bugs that were found: + +BUG: +- Job identifier doesn't return immediately + +DESCRIPTION: +In the details of the assignment, it was stated that a POST to hash +should accept a password and it should return a job identifier immediately. +However, is usually takes over 5 seconds to return the job identifier. Furthermore, +it seems that the job identifier if only returned after the hash has already been computed. +Meaning, there does not seem to be a need to wait five seconds before using the +job identifier to get the hash. + +BUG: +- Password is Encoded and/or hashed incorrectly + +DESCRIPTION: +- In the details of the assignment, it was stated that A GET to the hash endpoint should return the +base64 encoded password hash. The GET request returns the encoded password hash; however, +it does not match up to the expected value. Since the SHA512 hash cannot be seen, it is unsure whether +the SHA512 or the base64 is the problem. Furthermore, it seems like the key may be used in the hashing and or +encoding. Reason why this is suspected is because of the following example: + +Body Passed in POST requet to hash: {"password": "angrymonkey"} +Encoded Password Hash: NN0PAKtieayiTY8/Qd53AeMzHkbvZDdwYYiDnwtDdv/FIWvcy1sKCb7qi7Nu8Q8Cd/MqjQeyCI0pWKDGp74A1g== + +Body Passed in POST requet to hash: {"p": "angrymonkey"} +Encoded Password Hash: z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg== + +These return two different results. If only the password was being used the hashes should be indentical. + +BUG: +- Empty string is allowed as a password + +DESCRIPTION: +- A POST to the hash endpoint should require a password. + +BUG: +- A different key in place of password can be passed in A POST to hash endpoint + +DESCRIPTION: +- There should be validation in some regards as to what is passed in the body of +the POST request to the /hash endpoint + +BUG: +- Empty JSON is allowed to be passed to the hash endpoint on a POST request + +DESCRIPTION: +- Empty JSON should not be allowed to be passed to the hash endpoint on a POST request. +There should be some validation around this. + +BUG: +- Stats endpoint supports POST method + +DESCRIPTION: +- In the details of the assignment, it said that only three endpoints should be supported. +There should be validation against using POST on the stats endpoint. The http error code +should return with a 405 which is method not supported. + +BUG: +- Params allowed to be passed to the stats endpoint on GET request + +DESCRIPTION: +- I am not exactly sure if this is a bug. In the details of the assignment it stated +that A GET to the stats endpoint should accept no data. I took this in terms of that +it should not allow parameters to be passed in. Also, I am not sure if headers are allowed +to be passed or not. + +BUG: +- Average Time of a hash request is not calculated correctly + +DESCRIPTION: +- In the details of the assignment it stated that average time was the average time +of a hash request. The value that I am getting is about 5 seconds; however, the value that +the application is returning is on average of 80 to 100 seconds or more. I am not sure if this +is because the value is not actually recorded in milliseconds or if the formula is wrong or if +there is something that I am missing in my formula or understanding. I only time the request of a POST +to hash and add up all times for all posts and then divide by the number of posts. + +BUG: +- Shutting down does not restart the process immediately + +DESCRIPTION: +- I am not sure if this is a bug or just a lack of understanding of how long it takes +to restart the process. In the details of the assignment it stated that Since this is a +hosted binary, you will have to detect shutdowns by checking if the previous data has +been erased (Heroku will immediately restart the process after a shutdown). This made it +seem like everything would be back up and running again in a few seconds. However, it seems +to take a long time for the restart to actually happen and actually throws a 503 error for quite +awhile. Even when it seems like there are no on going hashings or requests. diff --git a/JumpCloud QA Assignment/requirements.txt b/JumpCloud QA Assignment/requirements.txt new file mode 100644 index 0000000..5ad6623 --- /dev/null +++ b/JumpCloud QA Assignment/requirements.txt @@ -0,0 +1,14 @@ +The following libraries were used: +- multiprocessing +- json +- hashlib +- base64 +- time +- requests + +Useful links to libraries used: +- Requests Library +-- https://media.readthedocs.org/pdf/requests/latest/requests.pdf +-- http://docs.python-requests.org/en/master/user/quickstart/ +- Multiprocessing Library +-- https://docs.python.org/2/library/multiprocessing.html#multiprocessing.Queue