Skip to content

Commit

Permalink
Merge branch 'next' into feat/api-signup
Browse files Browse the repository at this point in the history
  • Loading branch information
krestenlaust authored Jan 15, 2025
2 parents 202fc8c + acf6b47 commit bd0fc87
Show file tree
Hide file tree
Showing 19 changed files with 2,155 additions and 311 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: OpenAPI Validation & HTTP Test

on:
push:
branches:
- master
- next
pull_request:

jobs:
build:

runs-on: ubuntu-20.04
strategy:
max-parallel: 4
matrix:
python-version: ["3.11.9"]

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r openapi/requirements.txt
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm i -g dredd
- run: dredd --config openapi/dredd.yml
32 changes: 32 additions & 0 deletions openapi/dredd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
color: true
dry-run: null
hookfiles: ["openapi/dredd_hook.py"]
language: python
require: null
server: "python3 manage.py testserver stregsystem/fixtures/openapi-fixture.json"
server-wait: 5
init: false
custom: {}
names: false
only: []
reporter: apiary
output: []
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
loglevel: warn
path: []
hooks-worker-timeout: 5000
hooks-worker-connect-timeout: 1500
hooks-worker-connect-retry: 500
hooks-worker-after-connect-wait: 100
hooks-worker-term-timeout: 5000
hooks-worker-term-retry: 500
hooks-worker-handler-host: 127.0.0.1
hooks-worker-handler-port: 61321
config: ./dredd.yml
blueprint: openapi/stregsystem.yaml
endpoint: 'http://127.0.0.1:8000'
42 changes: 42 additions & 0 deletions openapi/dredd_hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import dredd_hooks as hooks
import json
from utils import update_query_parameter_values, update_dictionary_values

not_found_parameter_values = {
'room_id': 1,
'member_id': 1,
'username': "404_user",
}

skipped_endpoints = [
"GET (400) /api/member/payment/qr?username=kresten" # Skipped: test can't be implemented properly in OpenAPI
]

@hooks.before_each
def skip_endpoint(transaction):
if transaction['id'] in skipped_endpoints:
print(f"Skipping endpoint: {transaction['id']}")
transaction['skip'] = True


# https://dredd.org/en/latest/data-structures.html#transaction-object
@hooks.before_each
def replace_4xx_parameter_values(transaction):
"""
It isn't possible to specify individual parameter example values for each response type in OpenAPI.
To properly test the return value of not-found parameters, replace all parameters.
"""
if transaction['expected']['statusCode'][0] == '4':
new_path = update_query_parameter_values(transaction['fullPath'], not_found_parameter_values)
print(f"Update endpoint path, from '{transaction['fullPath']}' to '{new_path}'")
transaction['fullPath'] = new_path
transaction['request']['uri'] = new_path


@hooks.before_each
def replace_body_in_post_requests(transaction):
if transaction['expected']['statusCode'][0] == '4' and transaction['id'].startswith("POST"):
body = json.loads(transaction['request']['body'])
update_dictionary_values(body, not_found_parameter_values)

transaction['request']['body'] = json.dumps(body)
1 change: 1 addition & 0 deletions openapi/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dredd_hooks
80 changes: 48 additions & 32 deletions openapi/stregsystem.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,27 +237,33 @@ components:
required: true
schema:
$ref: '#/components/schemas/member_id'
example: 321
room_id_param:
name: room_id
in: query
description: ID of the room to retrieve.
required: true
schema:
$ref: '#/components/schemas/room_id'
example: 10
username_param:
name: username
in: query
description: Username of the member.
required: true
schema:
$ref: '#/components/schemas/username'
example: kresten
amount_param:
name: amount
in: query
description: Amount of money in streg-oere.
required: false
schema:
$ref: '#/components/schemas/stregoere_balance'
examples:
normalBalance:
value: 20000
schemas:
memberNotFoundMessage:
type: string
Expand All @@ -277,7 +283,7 @@ components:
missingRoomIdMessage:
type: string
example: "Parameter missing: room_id"
missingMemberUsername:
missingMemberUsernameMessage:
type: string
example: "Parameter missing: username"
usernameTakenMessage:
Expand Down Expand Up @@ -346,12 +352,20 @@ components:
stregoere_price:
type: integer
example: 600
stregoere_price_three_beers:
type: integer
example: 1800
stregoere_balance:
type: integer
example: 15000
stregoere_due:
type: integer
example: 20000
stregkroner_balance:
description: Stregbalance in kroner, only used in API-Sale
type: number
format: float
example: 182.00
signup_id:
type: integer
example: 5
Expand Down Expand Up @@ -391,36 +405,26 @@ components:
$ref: '#/components/schemas/product_name'
price:
$ref: '#/components/schemas/stregoere_price'
active_products_example:
type: object
properties:
123:
$ref: '#/components/schemas/active_product'
buystring:
type: string
example: "kresten beer:3"
category_name:
product_category_name:
type: string
example: "Alcohol"
category_id:
product_category_id:
type: integer
example: 11
category:
product_category:
type: object
properties:
category_id:
$ref: '#/components/schemas/category_id'
$ref: '#/components/schemas/product_category_id'
category_name:
$ref: '#/components/schemas/category_name'
category_mapping:
$ref: '#/components/schemas/product_category_name'
product_category_mapping:
type: array
items:
$ref: '#/components/schemas/category'
category_mappings_example:
type: object
properties:
123:
$ref: '#/components/schemas/category_mapping'
$ref: '#/components/schemas/product_category'
created_on:
type: string
format: date
Expand All @@ -433,13 +437,15 @@ components:
type: boolean
example: false
bp_minutes:
description: Ballmer Peak minutes
type: integer
nullable: true
example: 2
example: null
bp_seconds:
description: Ballmer Peak seconds
type: integer
nullable: true
example: 30
example: null
caffeine:
type: integer
example: 2
Expand All @@ -457,7 +463,7 @@ components:
example: true
sale_hints:
type: string
example: "<span class=\"username\">kresten</span> beer:3"
example: "<span class=\"username\">kresten</span> 123:3"
member_has_low_balance:
type: boolean
example: false
Expand All @@ -484,6 +490,10 @@ components:
type: array
items:
$ref: '#/components/schemas/product_id'
example:
- 123
- 123
- 123
promille:
$ref: '#/components/schemas/promille'
is_ballmer_peaking:
Expand All @@ -501,15 +511,15 @@ components:
is_coffee_master:
$ref: '#/components/schemas/is_coffee_master'
cost:
$ref: '#/components/schemas/stregoere_price'
$ref: '#/components/schemas/stregoere_price_three_beers'
give_multibuy_hint:
$ref: '#/components/schemas/give_multibuy_hint'
sale_hints:
$ref: '#/components/schemas/sale_hints'
member_has_low_balance:
$ref: '#/components/schemas/member_has_low_balance'
member_balance:
$ref: '#/components/schemas/stregoere_balance'
$ref: '#/components/schemas/stregkroner_balance'
sale:
type: object
properties:
Expand Down Expand Up @@ -593,19 +603,25 @@ components:
content:
application/json:
example:
$ref: '#/components/schemas/named_products_example'
beer: 123
ActiveProducts:
description: Dictionary of all activated products, with their name and price (in stregører).
content:
application/json:
example:
$ref: '#/components/schemas/active_products_example'
123:
name: Beer
price: 600
CategoryMappings:
description: Dictionary of all activated products, with their mapped categories (both category name and ID).
content:
application/json:
example:
$ref: '#/components/schemas/category_mappings_example'
123:
- category_id:
11
category_name:
"Alcohol"
SaleSuccess:
description: An object containing various statistics and info regarding the purchase.
content:
Expand Down Expand Up @@ -645,18 +661,18 @@ components:
InvalidQRInputResponse:
description: Invalid input has been provided.
content:
text/html:
text/html; charset=utf-8:
schema:
type: string
example: Invalid input for MobilePay QR code generation
MemberUsernameParameter_BadResponse:
description: Member does not exist, or missing parameter.
content:
text/html:
text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/memberNotFoundMessage'
- $ref: '#/components/schemas/missingMemberUsername'
- $ref: '#/components/schemas/missingMemberUsernameMessage'
examples:
memberNotFound:
$ref: '#/components/examples/MemberNotFoundExample'
Expand All @@ -665,7 +681,7 @@ components:
MemberIdParameter_BadResponse:
description: Member does not exist, invalid member ID, or missing parameter.
content:
text/html:
text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/memberNotFoundMessage'
Expand All @@ -681,7 +697,7 @@ components:
RoomIdParameter_BadResponse:
description: Room does not exist, invalid room ID, or missing parameter.
content:
text/html:
text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/roomNotFoundMessage'
Expand All @@ -697,7 +713,7 @@ components:
Member_RoomIdParameter_BadResponse:
description: Room or member does not exist, invalid room or member ID, or missing parameter.
content:
text/html:
text/html; charset=utf-8:
schema:
oneOf:
- $ref: '#/components/schemas/memberNotFoundMessage'
Expand Down
31 changes: 31 additions & 0 deletions openapi/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Any
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse


def update_dictionary_values(original: dict[Any, Any], replacement: dict[Any, Any]) -> None:
"""
Same as dict.update(...) but doesn't add new keys.
Mutates 'original'
:param original: The dictionary to mutate.
:param replacement: The dictionary with values to update the original with.
"""
original.update({k: v for k, v in replacement.items() if k in original})


def update_query_parameter_values(url_string: str, new_parameter_values: dict[str, Any]) -> str:
"""
Updates query parameters with new parameter values from new_parameter_values.
:param url_string: The URL path of which to modify query parameters.
:param new_parameter_values: The dictionary with new query parameter values.
:return: The URL with updated query parameter.
"""
parsed_url = urlparse(url_string)

qs_dict = parse_qs(parsed_url.query, keep_blank_values=True)
qs_dict_flattened = {key: value[0] for key, value in qs_dict.items()}
update_dictionary_values(qs_dict_flattened, new_parameter_values)
updated_qs = urlencode(qs_dict_flattened, doseq=False)

parsed_url = parsed_url._replace(query=updated_qs)

return str(urlunparse(parsed_url))
Loading

0 comments on commit bd0fc87

Please sign in to comment.