Skip to content

Commit e4c322b

Browse files
authored
add infra tests using pytest and github actions (#3)
1 parent 196cba5 commit e4c322b

File tree

3 files changed

+329
-0
lines changed

3 files changed

+329
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
name: Run Integration Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
schedule:
11+
# “At 00:00 on Sunday.”
12+
- cron: "0 0 * * 0"
13+
workflow_dispatch:
14+
inputs:
15+
runner-os:
16+
default: ubuntu-latest
17+
type: choice
18+
options:
19+
- ubuntu-latest
20+
21+
22+
jobs:
23+
run-it-tests-job:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- name: Checkout
27+
uses: actions/checkout@v4
28+
29+
- name: Set up Python 3.11
30+
id: setup-python
31+
uses: actions/setup-python@v2
32+
with:
33+
python-version: 3.11
34+
35+
- name: Setup Node.js
36+
uses: actions/setup-node@v4
37+
with:
38+
node-version: 20
39+
40+
- name: Set up Dependencies
41+
run: |
42+
pip install requests boto3 pytest
43+
44+
- name: Start LocalStack
45+
uses: LocalStack/setup-localstack@v0.2.3
46+
with:
47+
image-tag: 'latest'
48+
use-pro: 'true'
49+
configuration: LS_LOG=trace
50+
install-awslocal: 'true'
51+
env:
52+
LOCALSTACK_API_KEY: ${{ secrets.LOCALSTACK_API_KEY }}
53+
54+
- name: Deploy infrastructure
55+
run: |
56+
bash bin/deploy.sh
57+
58+
- name: Run Tests
59+
env:
60+
AWS_DEFAULT_REGION: us-east-1
61+
AWS_REGION: us-east-1
62+
AWS_ACCESS_KEY_ID: test
63+
AWS_SECRET_ACCESS_KEY: test
64+
run: |
65+
pytest -v
66+
67+
- name: Show localstack logs
68+
if: always()
69+
run: |
70+
localstack logs
71+
72+
- name: Send a Slack notification
73+
if: failure() || github.event_name != 'pull_request'
74+
uses: ravsamhq/notify-slack-action@v2
75+
with:
76+
status: ${{ job.status }}
77+
token: ${{ secrets.GITHUB_TOKEN }}
78+
notification_title: "{workflow} has {status_message}"
79+
message_format: "{emoji} *{workflow}* {status_message} in <{repo_url}|{repo}>"
80+
footer: "Linked Repo <{repo_url}|{repo}> | <{run_url}|View Workflow run>"
81+
notify_when: "failure"
82+
env:
83+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
84+
85+
- name: Generate a Diagnostic Report
86+
if: failure()
87+
run: |
88+
curl -s localhost:4566/_localstack/diagnose | gzip -cf > diagnose.json.gz
89+
90+
- name: Upload the Diagnostic Report
91+
if: failure()
92+
uses: actions/upload-artifact@v3
93+
with:
94+
name: diagnose.json.gz
95+
path: ./diagnose.json.gz

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ dist/
1212
.venv
1313
env/
1414
venv/
15+
__pycache__

tests/test_infra.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import pytest
2+
import boto3
3+
import requests
4+
import json
5+
import time
6+
7+
@pytest.fixture(scope='module')
8+
def api_endpoint():
9+
apigateway_client = boto3.client('apigateway', endpoint_url='http://localhost:4566')
10+
lambda_client = boto3.client('lambda', endpoint_url='http://localhost:4566')
11+
12+
API_NAME = 'QuizAPI'
13+
response = apigateway_client.get_rest_apis()
14+
api_list = response.get('items', [])
15+
api = next((item for item in api_list if item['name'] == API_NAME), None)
16+
17+
if not api:
18+
raise Exception(f"API {API_NAME} not found.")
19+
20+
API_ID = api['id']
21+
API_ENDPOINT = f"http://localhost:4566/restapis/{API_ID}/test/_user_request_"
22+
23+
print(f"API Endpoint: {API_ENDPOINT}")
24+
25+
time.sleep(2)
26+
27+
return API_ENDPOINT
28+
29+
def test_quiz_workflow(api_endpoint):
30+
create_quiz_payload = {
31+
"Title": "Sample Quiz",
32+
"Visibility": "Public",
33+
"EnableTimer": True,
34+
"TimerSeconds": 10,
35+
"Questions": [
36+
{
37+
"QuestionText": "What is the capital of France?",
38+
"Options": ["A. Berlin", "B. London", "C. Madrid", "D. Paris"],
39+
"CorrectAnswer": "D. Paris",
40+
"Trivia": "Paris is known as the City of Light."
41+
},
42+
{
43+
"QuestionText": "Who wrote Hamlet?",
44+
"Options": ["A. Dickens", "B. Shakespeare", "C. Twain", "D. Hemingway"],
45+
"CorrectAnswer": "B. Shakespeare",
46+
"Trivia": "Shakespeare is often called England's national poet."
47+
},
48+
{
49+
"QuestionText": "What is the largest planet in our solar system?",
50+
"Options": ["A. Earth", "B. Mars", "C. Jupiter", "D. Saturn"],
51+
"CorrectAnswer": "C. Jupiter",
52+
"Trivia": "Jupiter is so large that all the other planets in the solar system could fit inside it."
53+
},
54+
{
55+
"QuestionText": "Which element has the chemical symbol 'O'?",
56+
"Options": ["A. Gold", "B. Oxygen", "C. Silver", "D. Iron"],
57+
"CorrectAnswer": "B. Oxygen",
58+
"Trivia": "Oxygen makes up about 21% of the Earth's atmosphere."
59+
},
60+
{
61+
"QuestionText": "In which year did World War II end?",
62+
"Options": ["A. 1943", "B. 1945", "C. 1947", "D. 1950"],
63+
"CorrectAnswer": "B. 1945",
64+
"Trivia": "The war ended with the surrender of Japan on September 2, 1945."
65+
}
66+
]
67+
}
68+
69+
response = requests.post(
70+
f"{api_endpoint}/createquiz",
71+
headers={"Content-Type": "application/json"},
72+
data=json.dumps(create_quiz_payload)
73+
)
74+
75+
assert response.status_code == 200
76+
quiz_creation_response = response.json()
77+
assert 'QuizID' in quiz_creation_response
78+
quiz_id = quiz_creation_response['QuizID']
79+
80+
print(f"Quiz created with ID: {quiz_id}")
81+
82+
response = requests.get(f"{api_endpoint}/listquizzes")
83+
assert response.status_code == 200
84+
quizzes_list = response.json().get('Quizzes', [])
85+
quiz_titles = [quiz['Title'] for quiz in quizzes_list]
86+
assert "Sample Quiz" in quiz_titles
87+
88+
response = requests.get(f"{api_endpoint}/getquiz?quiz_id={quiz_id}")
89+
assert response.status_code == 200
90+
quiz_details = response.json()
91+
assert quiz_details['Title'] == "Sample Quiz"
92+
assert len(quiz_details['Questions']) == 5
93+
94+
submissions = []
95+
users = [
96+
{
97+
"Username": "user1",
98+
"Answers": {
99+
"0": {"Answer": "D. Paris", "TimeTaken": 8},
100+
"1": {"Answer": "B. Shakespeare", "TimeTaken": 5},
101+
"2": {"Answer": "C. Jupiter", "TimeTaken": 6},
102+
"3": {"Answer": "B. Oxygen", "TimeTaken": 7},
103+
"4": {"Answer": "B. 1945", "TimeTaken": 9}
104+
}
105+
},
106+
{
107+
"Username": "user2",
108+
"Email": "user@example.com",
109+
"Answers": {
110+
"0": {"Answer": "D. Paris", "TimeTaken": 7},
111+
"1": {"Answer": "B. Shakespeare", "TimeTaken": 6},
112+
"2": {"Answer": "D. Saturn", "TimeTaken": 5}, # Incorrect
113+
"3": {"Answer": "B. Oxygen", "TimeTaken": 8},
114+
"4": {"Answer": "B. 1945", "TimeTaken": 10}
115+
}
116+
},
117+
{
118+
"Username": "user3",
119+
"Answers": {
120+
"0": {"Answer": "A. Berlin", "TimeTaken": 9}, # Incorrect
121+
"1": {"Answer": "D. Hemingway", "TimeTaken": 4}, # Incorrect
122+
"2": {"Answer": "C. Jupiter", "TimeTaken": 11}, # Exceeds time
123+
"3": {"Answer": "B. Oxygen", "TimeTaken": 12}, # Exceeds time
124+
"4": {"Answer": "B. 1945", "TimeTaken": 13} # Exceeds time
125+
}
126+
}
127+
]
128+
129+
for user in users:
130+
submission_payload = {
131+
"Username": user["Username"],
132+
"QuizID": quiz_id,
133+
"Answers": user["Answers"]
134+
}
135+
if "Email" in user:
136+
submission_payload["Email"] = user["Email"]
137+
138+
response = requests.post(
139+
f"{api_endpoint}/submitquiz",
140+
headers={"Content-Type": "application/json"},
141+
data=json.dumps(submission_payload)
142+
)
143+
assert response.status_code == 200
144+
submission_response = response.json()
145+
assert 'SubmissionID' in submission_response
146+
submissions.append({
147+
"Username": user["Username"],
148+
"SubmissionID": submission_response["SubmissionID"]
149+
})
150+
151+
print(f"{user['Username']} submitted quiz with SubmissionID: {submission_response['SubmissionID']}")
152+
153+
time.sleep(5)
154+
155+
response = requests.get(f"{api_endpoint}/getleaderboard?quiz_id={quiz_id}&top=3")
156+
assert response.status_code == 200
157+
leaderboard = response.json()
158+
assert len(leaderboard) == 3
159+
160+
expected_scores = {
161+
"user1": None,
162+
"user2": None,
163+
"user3": None
164+
}
165+
166+
max_score = 100
167+
timer_seconds = 10
168+
169+
correct_answers = ["D. Paris", "B. Shakespeare", "C. Jupiter", "B. Oxygen", "B. 1945"]
170+
171+
def calculate_user_score(user_answers):
172+
score = 0
173+
for idx, correct_answer in enumerate(correct_answers):
174+
user_answer = user_answers[str(idx)]["Answer"]
175+
time_taken = user_answers[str(idx)]["TimeTaken"]
176+
if user_answer == correct_answer and time_taken <= timer_seconds:
177+
question_score = max_score * (1 - (time_taken / timer_seconds))
178+
score += max(0, question_score)
179+
else:
180+
pass
181+
return score
182+
183+
for user in users:
184+
username = user["Username"]
185+
expected_scores[username] = calculate_user_score(user["Answers"])
186+
187+
for entry in leaderboard:
188+
username = entry["Username"]
189+
actual_score = entry["Score"]
190+
expected_score = expected_scores[username]
191+
assert actual_score == pytest.approx(expected_score, abs=0.01)
192+
193+
print(f"{username} - Expected Score: {expected_score}, Actual Score: {actual_score}")
194+
195+
for submission in submissions:
196+
response = requests.get(f"{api_endpoint}/getsubmission?submission_id={submission['SubmissionID']}")
197+
assert response.status_code == 200
198+
submission_data = response.json()
199+
assert submission_data['Username'] == submission['Username']
200+
assert submission_data['QuizID'] == quiz_id
201+
assert 'Score' in submission_data
202+
assert 'UserAnswers' in submission_data
203+
204+
expected_score = expected_scores[submission['Username']]
205+
actual_score = submission_data['Score']
206+
assert actual_score == pytest.approx(expected_score, abs=0.01)
207+
208+
print(f"Verified submission for {submission['Username']} with Score: {actual_score}")
209+
210+
ses_endpoint = "http://localhost:4566/_aws/ses"
211+
ses_response = requests.get(ses_endpoint)
212+
assert ses_response.status_code == 200
213+
ses_data = ses_response.json()
214+
215+
messages = ses_data.get('messages', [])
216+
sender_email = "sender@example.com"
217+
email_found = False
218+
219+
for message in messages:
220+
if message.get('Source') == sender_email:
221+
email_found = True
222+
assert 'Id' in message
223+
assert 'Region' in message
224+
assert 'Timestamp' in message
225+
assert 'Destination' in message
226+
assert 'Subject' in message
227+
assert 'Body' in message
228+
229+
body = message['Body']
230+
assert 'html_part' in body
231+
html_content = body['html_part']
232+
233+
assert email_found, f"No email found sent from {sender_email}"

0 commit comments

Comments
 (0)