Skip to content

Commit

Permalink
Add E2E tests
Browse files Browse the repository at this point in the history
  • Loading branch information
d3QUone committed Dec 21, 2023
1 parent 90b3bd8 commit 1a6b802
Show file tree
Hide file tree
Showing 14 changed files with 243 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .env_example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
AWS_REGION_NAME=""
AWS_ACCESS_KEY_ID=""
AWS_SECRET_ACCESS_KEY=""
AWS_HOST=""
AWS_QUEUE_NAME=""
21 changes: 20 additions & 1 deletion MAINTAIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@ poetry install
poetry run pre-commit install
```

Run tests:

## Unit-tests

```shell
make test
```


## E2E tests

To run integration (or end-to-end) tests on a real instance of Amazon SQS put your credentials in local file:
```shell
cp .env_example .env
```

Run special tests after `.env` file is ready:
```shell
make test_e2e
```


# Build and publish

Update package version manually inside `aiosqs/__init__.py` file and inside `pyproject.toml` file.

Then install a tool to publish:
```shell
python3 -m pip install hatch
```
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#!/bin/bash
VERBOSE=1

# Run only unit tests in CI
test:
poetry run nose2 --verbosity $(VERBOSE) $(target)
poetry run nose2 -s aiosqs --verbosity $(VERBOSE) $(target)

# Run E2E tests locally during development
test_e2e:
poetry run nose2 -s e2e --verbosity $(VERBOSE) $(target)
11 changes: 8 additions & 3 deletions aiosqs/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from aiosqs.encryption import get_signature_key, hmac_sha256_hexdigest, sha256_hexdigest
from aiosqs.exceptions import SQSClientBaseError
from aiosqs.types import LoggerType, GetQueueUrlResponse, ReceiveMessageResponse
from aiosqs.types import LoggerType, GetQueueUrlResponse, ReceiveMessageResponse, SendMessageResponse
from aiosqs.parser import parse_xml_result_response

default_logger = getLogger(__name__)
Expand Down Expand Up @@ -144,7 +144,12 @@ async def request(self, params: Dict) -> Union[Dict, List, None]:
raise SQSClientBaseError

if not response.ok:
self.logger.error(f"SQS API error: status_code=%s, body=%s", response.status, response_body)
status_code = response.status
self.logger.error(f"SQS API error: status_code=%s, body=%s", status_code, response_body)

if "ErrorResponse" in response_body:
return parse_xml_result_response(action=params["Action"], body=response_body, logger=self.logger)

raise SQSClientBaseError

return parse_xml_result_response(action=params["Action"], body=response_body, logger=self.logger)
Expand All @@ -156,7 +161,7 @@ async def get_queue_url(self, queue_name: str) -> GetQueueUrlResponse:
}
return await self.request(params=params)

async def send_message(self, queue_url: str, message_body: str, delay_seconds: int = 0):
async def send_message(self, queue_url: str, message_body: str, delay_seconds: int = 0) -> SendMessageResponse:
params = {
"Action": "SendMessage",
"DelaySeconds": delay_seconds,
Expand Down
5 changes: 4 additions & 1 deletion aiosqs/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ class ErrorData:


class SQSClientBaseError(Exception):
"""Generic error, including transportation errors."""
"""Generic error, including transportation errors.
Full list of raw errors:
https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/CommonErrors.html
"""


class SQSErrorResponse(SQSClientBaseError):
Expand Down
9 changes: 9 additions & 0 deletions aiosqs/tests/fixtures/error_invalid_access_key.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
<Error>
<Type>Sender</Type>
<Code>InvalidClientTokenId</Code>
<Message>key_id is invalid</Message>
<Details/>
</Error>
<RequestId>4622a4ae-1166-48d6-bf50-2091ab4caafb</RequestId>
</ErrorResponse>
9 changes: 9 additions & 0 deletions aiosqs/tests/fixtures/error_invalid_secret_key.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
<Error>
<Type>Sender</Type>
<Code>InvalidClientTokenId</Code>
<Message>The security token included in the request is invalid.</Message>
<Details/>
</Error>
<RequestId>d4b992fa-9b39-4176-9e11-864b040ebbf3</RequestId>
</ErrorResponse>
27 changes: 27 additions & 0 deletions aiosqs/tests/fixtures/error_signature.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
<Error>
<Type>Sender</Type>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.

The Canonical String for this request should have been
'GET
/
Action=SendMessage&amp;DelaySeconds=0&amp;MessageBody=message%20%20with%20a%20space&amp;QueueUrl=https%3A%2F%2Fsqs.us-east-1.amazonaws.com%2F763215857860%2Fcontent-repository-processing-queue-prod&amp;Version=2012-11-05
host:sqs.us-east-1.amazonaws.com
x-amz-date:20231205T232537Z

host;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'

The String-to-Sign should have been
'AWS4-HMAC-SHA256
20231205T232537Z
20231205/us-east-1/sqs/aws4_request
4ea765b1b277df9a9b929ee3383f75060631e8f75fb38b9c8b8b05a84c009474'
</Message>
<Detail/>
</Error>
<RequestId>f679cf26-effe-5e59-81ca-92cf5c4c713b</RequestId>
</ErrorResponse>
50 changes: 50 additions & 0 deletions aiosqs/tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import unittest
import re
import logging

from aioresponses import aioresponses

from aiosqs.exceptions import SQSErrorResponse
from aiosqs.client import SQSClient
from aiosqs.tests.utils import load_fixture


class ClientTestCase(unittest.IsolatedAsyncioTestCase):
async def asyncSetUp(self):
await super().asyncSetUp()

logger = logging.getLogger(__name__)
logger.setLevel(logging.CRITICAL)

self.client = SQSClient(
aws_access_key_id="access_key_id",
aws_secret_access_key="secret_access_key",
region_name="us-west-2",
host="mocked_amazon_host.com",
timeout_sec=0,
logger=logger,
)

async def asyncTearDown(self):
await self.client.close()

@aioresponses()
async def test_is_context_manager(self, mock):
mock.get(
Expand All @@ -25,3 +45,33 @@ async def test_is_context_manager(self, mock):
) as client:
response = await client.get_queue_url(queue_name="mocked_queue_name")
self.assertEqual(response, {"QueueUrl": "http://sqs.mcs.mail.ru/account123/example_queue"})

@aioresponses()
async def test_invalid_access_key(self, mock):
mock.get(
url=re.compile(r"https://mocked_amazon_host.com"),
status=403,
body=load_fixture("error_invalid_access_key.xml"),
)

with self.assertRaises(SQSErrorResponse) as e:
await self.client.get_queue_url(queue_name="mocked_queue_name")
exception = e.exception
self.assertEqual(exception.error.type, "Sender")
self.assertEqual(exception.error.code, "InvalidClientTokenId")
self.assertEqual(exception.error.message, "key_id is invalid")

@aioresponses()
async def test_invalid_secret_key(self, mock):
mock.get(
url=re.compile(r"https://mocked_amazon_host.com"),
status=403,
body=load_fixture("error_invalid_secret_key.xml"),
)

with self.assertRaises(SQSErrorResponse) as e:
await self.client.get_queue_url(queue_name="mocked_queue_name")
exception = e.exception
self.assertEqual(exception.error.type, "Sender")
self.assertEqual(exception.error.code, "InvalidClientTokenId")
self.assertEqual(exception.error.message, "The security token included in the request is invalid.")
11 changes: 11 additions & 0 deletions aiosqs/tests/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,14 @@ def test_unknown_error(self):
exception.error.message,
"The request processing has failed because of an unknown error, exception or failure.",
)

def test_signature_does_not_match(self):
with self.assertRaises(SQSErrorResponse) as e:
parse_xml_result_response(
action="SendMessage",
body=load_fixture("error_signature.xml"),
)
exception = e.exception
self.assertEqual(exception.request_id, "f679cf26-effe-5e59-81ca-92cf5c4c713b")
self.assertEqual(exception.error.type, "Sender")
self.assertEqual(exception.error.code, "SignatureDoesNotMatch")
Empty file added e2e/__init__.py
Empty file.
76 changes: 76 additions & 0 deletions e2e/test_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import unittest
import logging

from dotenv import dotenv_values

from aiosqs import SQSClient, SQSErrorResponse


class E2ETestCase(unittest.IsolatedAsyncioTestCase):
@classmethod
def setUpClass(cls) -> None:
config = dotenv_values(".env")
cls.region_name = config["AWS_REGION_NAME"]
cls.aws_access_key_id = config["AWS_ACCESS_KEY_ID"]
cls.aws_secret_access_key = config["AWS_SECRET_ACCESS_KEY"]
cls.host = config["AWS_HOST"]
cls.queue_name = config["AWS_QUEUE_NAME"]

async def asyncSetUp(self):
await super().asyncSetUp()

logger = logging.getLogger(__name__)
logger.setLevel(logging.CRITICAL)

self.client = SQSClient(
region_name=self.region_name,
aws_access_key_id=self.aws_access_key_id,
aws_secret_access_key=self.aws_secret_access_key,
host=self.host,
verify_ssl=False,
logger=logger,
)

async def asyncTearDown(self):
await self.client.close()

async def test_get_queue_url(self):
response = await self.client.get_queue_url(queue_name=self.queue_name)
queue_url = response["QueueUrl"]
self.assertTrue(queue_url.endswith(self.queue_name), msg=queue_url)

async def test_call_unknown_queue_name(self):
with self.assertRaises(SQSErrorResponse) as e:
await self.client.get_queue_url(queue_name="ErrorDoesNotExistName")
exception = e.exception
self.assertEqual(exception.error.type, "Sender")
self.assertEqual(exception.error.code, "AWS.SimpleQueueService.NonExistentQueue")
self.assertEqual(exception.error.message, "The specified queue doesn't exist.")

async def test_send_message_with_raw_text(self):
response = await self.client.get_queue_url(queue_name=self.queue_name)
queue_url = response["QueueUrl"]

message_body = "a b c d"
response = await self.client.send_message(
queue_url=queue_url,
message_body=message_body,
)
self.assertEqual(set(response.keys()), {"MessageId", "MD5OfMessageBody"})

response = await self.client.receive_message(
queue_url=queue_url,
max_number_of_messages=1,
visibility_timeout=60,
)
self.assertTrue(len(response) > 0)

message = response[0]
receipt_handle = message["ReceiptHandle"]
self.assertEqual(message["Body"], message_body)

response = await self.client.delete_message(
queue_url=queue_url,
receipt_handle=receipt_handle,
)
self.assertIsNone(response)
16 changes: 15 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Source = "https://github.com/d3QUone/aiosqs"

[tool.poetry]
name = "aiosqs"
version = "0.1.0"
version = "1.0.3"
description = "Python asynchronous and lightweight SQS client."
authors = ["Vladimir Kasatkin <[email protected]>"]
license = "MIT"
Expand All @@ -60,6 +60,7 @@ freezegun = "1.2.2"
pre-commit = "2.17.0"
packaging = "23.2"
aioresponses = "0.7.6"
python-dotenv = "1.0.0"

[tool.black]
line-length = 140
Expand All @@ -85,4 +86,5 @@ exclude = [
"/MAINTAIN.md",
"/Makefile",
"/aiosqs/tests",
"/e2e",
]

0 comments on commit 1a6b802

Please sign in to comment.