From 69fb873e6c7b802e62e8f399de5ac7ecf23e8c12 Mon Sep 17 00:00:00 2001 From: Mosca Federico Date: Sat, 25 Jun 2022 17:25:27 +0200 Subject: [PATCH 1/3] e2e tests --- .gitignore | 1 + behave.ini | 7 ++++ requestbin/api.py | 3 +- requestbin/db.py | 4 +- requestbin/models.py | 7 +++- requestbin/storage/memory.py | 4 +- requestbin/storage/redis.py | 4 +- tests/README.md | 24 ++++++++++++ tests/features/environment.py | 67 +++++++++++++++++++++++++++++++++ tests/features/steps/test.py | 70 +++++++++++++++++++++++++++++++++++ tests/features/test.feature | 13 +++++++ tests/requirments.txt | 4 ++ 12 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 behave.ini create mode 100644 tests/README.md create mode 100644 tests/features/environment.py create mode 100644 tests/features/steps/test.py create mode 100644 tests/features/test.feature create mode 100644 tests/requirments.txt diff --git a/.gitignore b/.gitignore index 8c7afb8..9d1c8e9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ **/**/*.pyc *.egg-info **/*.pyc +.idea serviced.log codekit-config.json diff --git a/behave.ini b/behave.ini new file mode 100644 index 0000000..95b992d --- /dev/null +++ b/behave.ini @@ -0,0 +1,7 @@ +[behave] +stderr_capture=False +stdout_capture=False +log_capture = true +#junit=true +#format=behave_teamcity:TeamcityFormatter +format=plain \ No newline at end of file diff --git a/requestbin/api.py b/requestbin/api.py index 2a83065..6215c14 100644 --- a/requestbin/api.py +++ b/requestbin/api.py @@ -19,7 +19,8 @@ def _response(object, code=200): @app.endpoint('api.bins') def bins(): private = request.form.get('private') in ['true', 'on'] - bin = db.create_bin(private) + name = request.form.get('given_name', None) + bin = db.create_bin(private, name) if bin.private: session[bin.name] = bin.secret_key return _response(bin.to_dict()) diff --git a/requestbin/db.py b/requestbin/db.py index e1ca0be..0cb7461 100644 --- a/requestbin/db.py +++ b/requestbin/db.py @@ -15,8 +15,8 @@ db = klass(bin_ttl) -def create_bin(private=False): - return db.create_bin(private) +def create_bin(private=False, custon_name=None): + return db.create_bin(private, custon_name) def create_request(bin, request): return db.create_request(bin, request) diff --git a/requestbin/models.py b/requestbin/models.py index 8b3ba03..c7b02ac 100644 --- a/requestbin/models.py +++ b/requestbin/models.py @@ -16,11 +16,14 @@ class Bin(object): max_requests = config.MAX_REQUESTS - def __init__(self, private=False): + def __init__(self, private=False, name=None): self.created = time.time() self.private = private self.color = random_color() - self.name = tinyid(8) + if name is None: + self.name = tinyid(8) + else: + self.name = name self.favicon_uri = solid16x16gif_datauri(*self.color) self.requests = [] self.secret_key = os.urandom(24) if self.private else None diff --git a/requestbin/storage/memory.py b/requestbin/storage/memory.py index 8e8f54b..bacd7d2 100644 --- a/requestbin/storage/memory.py +++ b/requestbin/storage/memory.py @@ -27,8 +27,8 @@ def _expire_bins(self): if bin.created < expiry: self.bins.pop(name) - def create_bin(self, private=False): - bin = Bin(private) + def create_bin(self, private=False, given_name=None): + bin = Bin(private, name) self.bins[bin.name] = bin return self.bins[bin.name] diff --git a/requestbin/storage/redis.py b/requestbin/storage/redis.py index ae0df9a..28df9dd 100644 --- a/requestbin/storage/redis.py +++ b/requestbin/storage/redis.py @@ -22,8 +22,8 @@ def _key(self, name): def _request_count_key(self): return '{}-requests'.format(self.prefix) - def create_bin(self, private=False): - bin = Bin(private) + def create_bin(self, private=False, given_name=None): + bin = Bin(private, given_name) key = self._key(bin.name) self.redis.set(key, bin.dump()) self.redis.expireat(key, int(bin.created+self.bin_ttl)) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..bb6f2a3 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,24 @@ +# E2E tests + +## How to run the tests +### Setup +Create a python virtual env +```shell +virtualenv venv +``` +Activate it +```shell +source venv/bin/activate +``` +Install prerequisites +```shell +pip install -r tests/requirments.txt +``` +## Run the tests +```shell +behave tests/features/ +``` +The results are visible on stdout but also an output file is created +```shell +plain.output +``` \ No newline at end of file diff --git a/tests/features/environment.py b/tests/features/environment.py new file mode 100644 index 0000000..d4cc207 --- /dev/null +++ b/tests/features/environment.py @@ -0,0 +1,67 @@ +import requests +from hamcrest import * + +bin_name = "stuart" +uri = "http://localhost:8000" +payload = { + "data": { + "schedulePackage": { + "success": True, + "error": None, + "package": { + "id": "12345", + "deliveries": [ + { + "tasks": [ + { + "type": "PICKUP", + "address": { + "geocoded": "Carrer de Pau Claris 130, 08009, Barcelona, Spain", + "location": { + "lat": "41.39317", + "long": "2.16699" + } + } + }, + { + "type": "DROPOFF", + "address": { + "geocoded": "Carrer de Pau Claris 170, 08037, Barcelona, Spain", + "location": { + "lat": "41.39546", + "long": "2.16385" + } + } + } + ] + } + ], + "status": "NOT_ASSIGNED", + "createdAt": "2022-03-23T15:06:09+01:00", + "ref": "67890" + } + } + } +} + + +def before_all(context): + """ + Run before the tests + - create a named bin + - post the webhook to the bin + """ + context.vars = {'uri': uri, 'bin_name': bin_name} + + if requests.get('{}/{}'.format(context.vars['uri'], context.vars['bin_name'])).status_code != 200: + create_bin = requests.post('{}/api/v1/bins'.format(context.vars['uri']), data={"given_name": bin_name}) + assert_that(create_bin.status_code, equal_to(200)) + + if requests.get('{}/api/v1/bins/{}/requests'.format(context.vars['uri'], context.vars['bin_name']), + timeout=(5, 10)).status_code != 200: + # res = requests.get('{}/api/v1/bins/{}/requests'.format(context.vars['uri'], context.vars['bin_name']), + # timeout=(5, 10)) + # print(res.status_code) + + webhook = requests.post('{}/{}'.format(context.vars['uri'], context.vars['bin_name']), json=payload) + assert_that(webhook.status_code, equal_to(200)) diff --git a/tests/features/steps/test.py b/tests/features/steps/test.py new file mode 100644 index 0000000..8622a91 --- /dev/null +++ b/tests/features/steps/test.py @@ -0,0 +1,70 @@ +from behave import * +from hamcrest import * +import requests +import json + +use_step_matcher("re") + + +@given('The webhooks from a bin') +def step_get_webhook(context): + """Get webhooks from bin, wait 5 secs to connect and 10 to receive the data""" + try: + res = requests.get('{}/api/v1/bins/{}/requests'.format(context.vars['uri'], context.vars['bin_name']), + timeout=(5, 10)) + assert_that(res.status_code, equal_to(200)) + except requests.exceptions.ReadTimeout: + raise "Webhook not available after 10 seconds" + + webhook_ids = [] + + for req in res.json(): + if 'id' in req: + webhook_ids.append(req['id']) + context.vars['ids'] = webhook_ids + + +@when('I get the content of them') +def step_get_content(context): + """Get body of the webhooks""" + body = [] + for idx in context.vars['ids']: + res = requests.get('{}/api/v1/bins/{}/requests/{}'.format(context.vars['uri'], context.vars['bin_name'], idx)) + assert_that(res.status_code, equal_to(200)) + + if res.json()['content_length'] > 0: + print(idx) + body.append(json.loads(res.json()['body'])) + context.vars['body'] = body + + +@then('I check that in the "{name}" the key "{expected_value}" is present') +def step_check_body(context, name, expected_value): + """Check that the body contains a specific key""" + for datas in context.vars['body']: + assert_that(datas[name], has_key(expected_value)) + + +@then('I check that in (?P.*) (?P.*) (?Pis|is not) (?P.*)') +def step_check_data(context, name, key, check, expected_value): + """Check key are present and have a specific value""" + for val in context.vars['body']: + if name in val['data']: + assert_that(val['data'][name], has_key(key)) + if check == "is": + assert_that(str(val['data'][name][key]), equal_to(expected_value)) + else: + assert str(val['data'][name][key]) != expected_value + + +@then('I check that (?P.*) type in (?P.*) (?Pis|is not) a (?Parray|dictionary)') +def step_check_data(context, name, key, check, expected_type): + """Check key are present and the value is list or dict""" + type_checker = dict if expected_type == "dictionary" else list + for val in context.vars['body']: + if name in val['data']: + assert_that(val['data'][name], has_key(key)) + if check == "is": + assert_that(val['data'][name][key], instance_of(type_checker)) + else: + assert_that(val['data'][name][key], is_not(instance_of(type_checker))) diff --git a/tests/features/test.feature b/tests/features/test.feature new file mode 100644 index 0000000..507e640 --- /dev/null +++ b/tests/features/test.feature @@ -0,0 +1,13 @@ +Feature: Test webhook + + Scenario: Check webhook contains schedulePackage + Given The webhooks from a bin + When I get the content of them + Then I check that in the "data" the key "schedulePackage" is present + + Scenario: Check the package is been delivered without errors + Given The webhooks from a bin + When I get the content of them + Then I check that in schedulePackage success is True + And I check that in schedulePackage error is None + And I check that package type in schedulePackage is a dictionary diff --git a/tests/requirments.txt b/tests/requirments.txt new file mode 100644 index 0000000..78301b1 --- /dev/null +++ b/tests/requirments.txt @@ -0,0 +1,4 @@ +behave == 1.2.6 +requests == 2.27.1 +pyhamcrest == 1.10.1 +pycodestyle == 2.4.0 \ No newline at end of file From f2221adcf668ba7c13b2d084df6067850b6ba98c Mon Sep 17 00:00:00 2001 From: Mosca Federico Date: Sun, 26 Jun 2022 10:40:22 +0200 Subject: [PATCH 2/3] add pipeline, update readme and additional tests --- .gitignore | 2 +- .gitlab-ci.yml | 82 ++++++++++++++++++++++++++++++++++ behave.ini | 5 +-- pipeline.png | Bin 0 -> 46423 bytes tests/README.md | 46 ++++++++++++++++++- tests/features/environment.py | 12 ++--- tests/features/steps/test.py | 42 ++++++++++++++++- tests/features/test.feature | 13 ++++++ 8 files changed, 184 insertions(+), 18 deletions(-) create mode 100644 .gitlab-ci.yml create mode 100644 pipeline.png diff --git a/.gitignore b/.gitignore index 9d1c8e9..16a16de 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ **/*.pyc .idea serviced.log - +plain.output codekit-config.json # Mac Files diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..1bcedba --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,82 @@ +# PROPOSED pipeline +# prerequisites: if ubuntu:latest does not contain pip or docker, +# create an image install them and use this new image in the pipeline + +stages: + - build + - test + - deploy + - test-prod + +check-lint: + image: ubuntu:latest + stage: build + script: + - pip install -r tests/requirments.txt + - pycodestyle tests/ --max-line-length=120 + rules: + - if: $CI_PIPELINE_SOURCE == "push" + +docker-build: + image: ubuntu:latest + stage: build + script: + - docker build . --file Dockerfile + rules: + - if: $CI_PIPELINE_SOURCE == "push" + +e2e-test: + image: ubuntu:latest + stage: test + variables: + ENDPOINT: "http://localhost:8000" + script: + - docker-compose build && docker-compose up + - virtualenv venv + - source venv/bin/activate + - pip install -r tests/requirments.txt + - behave tests/features/ -D endpoint=$ENDPOINT + artifacts: + paths: + - plain.output + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + +deploy-prod: + image: ubuntu:latest + stage: deploy + script: echo "DEPLOY" # - commands to deploy in prod + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_BUILD_REF_NAME == "master"' + when: manual + +e2e-test-prod: + needs: + - deploy-prod + image: ubuntu:latest + stage: test-prod + variables: + ENDPOINT: "https://produrl" + script: + - virtualenv venv + - source venv/bin/activate + - pip install -r tests/requirments.txt + - behave tests/features/ --tags="@prod" -D endpoint=$ENDPOINT + artifacts: + paths: + - plain.output + rules: + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_BUILD_REF_NAME == "master"' + +non-functional-test: + image: ubuntu:latest + stage: test-prod + variables: + ENDPOINT: "https://produrl" + script: + - echo "non-functional tests" # commands to run non-functional tests + artifacts: + paths: + - test.output + rules: + - if: $CI_PIPELINE_SOURCE == "schedule" diff --git a/behave.ini b/behave.ini index 95b992d..97f3439 100644 --- a/behave.ini +++ b/behave.ini @@ -1,7 +1,4 @@ [behave] stderr_capture=False stdout_capture=False -log_capture = true -#junit=true -#format=behave_teamcity:TeamcityFormatter -format=plain \ No newline at end of file +format=plain diff --git a/pipeline.png b/pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..5c619e17cc85faf2c6ab7e93ee5587b3f554407b GIT binary patch literal 46423 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%0AGUjz#FQTd9hS{Ui6ZM}SDekjTF;fWROe=&!b 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 F{||vUQ!D@g literal 0 HcmV?d00001 diff --git a/tests/README.md b/tests/README.md index bb6f2a3..c350e98 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,9 +16,51 @@ pip install -r tests/requirments.txt ``` ## Run the tests ```shell -behave tests/features/ +behave tests/features/ -D endpoint="endpoint to test" ``` The results are visible on stdout but also an output file is created ```shell plain.output -``` \ No newline at end of file +``` + +## Lint +```shell + pycodestyle tests/ --max-line-length=120 +``` + +## Pipeline + +![pipeline](pipeline.png) + +I propose 4 stages for the pipeline +- Build +- Test +- Deploy +- Test-prod +#### Build +This stage is triggered and every push and consists in 2 jobs, +build the docker image and checking the codestytle for the testing project + +#### Test +In this stage the e2e tests run, it is building and running the docker container and +firing the e2e tests, this stage is triggered on a merge request event. +The results of the tests are available on the artifacts of the job, visible and downloadable on gitlab + +#### Deploy +This stage is a manual step to deploy on production. +It is available after the code is merged on master. +I decided to have this step manual for several reasons like: +- not every code change should be deployed immediately in production +- I imagined a release team responsible to deploy on production, that will also create a release note, + maybe also perform some manual check +- not all the engineer have the "power" to deploy on production + +Side note, I did not implemented the deploy scripts + +#### Test-prod +This stage is composed by 2 job. +- The first `e2e-test-prod` is triggered as soon as `deploy-prod` is finished. +It is running the e2e-tests but against the real endpoint that is specified on the job. +It is running only the scenarios that are tagged with `@prod` +- The second `non-functional-test` it is instead a scheduled job, so can be scheduled to run nightly/weekly/etc. +It runs non-functional test (performance) that I did not implemented in this homework but they should be taken in consideration diff --git a/tests/features/environment.py b/tests/features/environment.py index d4cc207..96ed516 100644 --- a/tests/features/environment.py +++ b/tests/features/environment.py @@ -2,7 +2,6 @@ from hamcrest import * bin_name = "stuart" -uri = "http://localhost:8000" payload = { "data": { "schedulePackage": { @@ -51,17 +50,12 @@ def before_all(context): - create a named bin - post the webhook to the bin """ + uri = context.config.userdata.get("endpoint", "http://localhost:9000") context.vars = {'uri': uri, 'bin_name': bin_name} if requests.get('{}/{}'.format(context.vars['uri'], context.vars['bin_name'])).status_code != 200: create_bin = requests.post('{}/api/v1/bins'.format(context.vars['uri']), data={"given_name": bin_name}) assert_that(create_bin.status_code, equal_to(200)) - if requests.get('{}/api/v1/bins/{}/requests'.format(context.vars['uri'], context.vars['bin_name']), - timeout=(5, 10)).status_code != 200: - # res = requests.get('{}/api/v1/bins/{}/requests'.format(context.vars['uri'], context.vars['bin_name']), - # timeout=(5, 10)) - # print(res.status_code) - - webhook = requests.post('{}/{}'.format(context.vars['uri'], context.vars['bin_name']), json=payload) - assert_that(webhook.status_code, equal_to(200)) + webhook = requests.post('{}/{}'.format(context.vars['uri'], context.vars['bin_name']), json=payload) + assert_that(webhook.status_code, equal_to(200)) diff --git a/tests/features/steps/test.py b/tests/features/steps/test.py index 8622a91..b884c90 100644 --- a/tests/features/steps/test.py +++ b/tests/features/steps/test.py @@ -9,6 +9,7 @@ @given('The webhooks from a bin') def step_get_webhook(context): """Get webhooks from bin, wait 5 secs to connect and 10 to receive the data""" + try: res = requests.get('{}/api/v1/bins/{}/requests'.format(context.vars['uri'], context.vars['bin_name']), timeout=(5, 10)) @@ -27,20 +28,24 @@ def step_get_webhook(context): @when('I get the content of them') def step_get_content(context): """Get body of the webhooks""" + body = [] for idx in context.vars['ids']: res = requests.get('{}/api/v1/bins/{}/requests/{}'.format(context.vars['uri'], context.vars['bin_name'], idx)) assert_that(res.status_code, equal_to(200)) if res.json()['content_length'] > 0: - print(idx) body.append(json.loads(res.json()['body'])) - context.vars['body'] = body + if body: + context.vars['body'] = body + else: + raise ValueError @then('I check that in the "{name}" the key "{expected_value}" is present') def step_check_body(context, name, expected_value): """Check that the body contains a specific key""" + for datas in context.vars['body']: assert_that(datas[name], has_key(expected_value)) @@ -48,6 +53,7 @@ def step_check_body(context, name, expected_value): @then('I check that in (?P.*) (?P.*) (?Pis|is not) (?P.*)') def step_check_data(context, name, key, check, expected_value): """Check key are present and have a specific value""" + for val in context.vars['body']: if name in val['data']: assert_that(val['data'][name], has_key(key)) @@ -60,6 +66,7 @@ def step_check_data(context, name, key, check, expected_value): @then('I check that (?P.*) type in (?P.*) (?Pis|is not) a (?Parray|dictionary)') def step_check_data(context, name, key, check, expected_type): """Check key are present and the value is list or dict""" + type_checker = dict if expected_type == "dictionary" else list for val in context.vars['body']: if name in val['data']: @@ -68,3 +75,34 @@ def step_check_data(context, name, key, check, expected_type): assert_that(val['data'][name][key], instance_of(type_checker)) else: assert_that(val['data'][name][key], is_not(instance_of(type_checker))) + + +@then('I verify that package contains (?Pid|deliveries)') +def step_check_package(context, expected_value): + """Check that id or deliveries are present in package""" + + for val in context.vars['body']: + package = val['data']['schedulePackage']['package'] + assert_that(package, has_key(expected_value)) + + +@then('I find the (?Ppickup|dropoff) address in deliveries') +def step_check_address(context, value): + """Check that there is 1 and only 1 pickup/dropoff address with coordinates per task""" + + for val in context.vars['body']: + if val['data']['schedulePackage']['success']: + deliveries = val['data']['schedulePackage']['package']['deliveries'] + for task in deliveries: + n_type = 0 + for t in task['tasks']: + if t['type'] == value.upper(): + n_type += 1 + assert_that(t, has_key('address')) + assert_that(t['address'], has_key('geocoded')) + assert_that(t['address'], has_key('location')) + assert_that(t['address']['location'], has_key('lat')) + assert_that(t['address']['location'], has_key('long')) + assert_that(n_type, equal_to(1)) + else: + raise ValueError diff --git a/tests/features/test.feature b/tests/features/test.feature index 507e640..dfc2dcc 100644 --- a/tests/features/test.feature +++ b/tests/features/test.feature @@ -11,3 +11,16 @@ Feature: Test webhook Then I check that in schedulePackage success is True And I check that in schedulePackage error is None And I check that package type in schedulePackage is a dictionary + + Scenario: Check the package contains id and deliveries + Given The webhooks from a bin + When I get the content of them + Then I verify that package contains id + And I verify that package contains deliveries + + @prod + Scenario: Check the deliveries has pickup and dropoff location + Given The webhooks from a bin + When I get the content of them + Then I find the pickup address in deliveries + And I find the dropoff address in deliveries From ea6b476961f807c427315b546be3a879c992ecf5 Mon Sep 17 00:00:00 2001 From: Mosca Federico Date: Sun, 26 Jun 2022 10:51:54 +0200 Subject: [PATCH 3/3] fix indentation --- tests/features/test.feature | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/features/test.feature b/tests/features/test.feature index dfc2dcc..a3a961d 100644 --- a/tests/features/test.feature +++ b/tests/features/test.feature @@ -1,9 +1,9 @@ Feature: Test webhook Scenario: Check webhook contains schedulePackage - Given The webhooks from a bin - When I get the content of them - Then I check that in the "data" the key "schedulePackage" is present + Given The webhooks from a bin + When I get the content of them + Then I check that in the "data" the key "schedulePackage" is present Scenario: Check the package is been delivered without errors Given The webhooks from a bin