Skip to content
This repository has been archived by the owner on Jul 26, 2024. It is now read-only.

Commit

Permalink
test: add capability of querying and clearing record of requests to p…
Browse files Browse the repository at this point in the history
…artner API (#415)

Co-authored-by: Raphael Pierzina <raphael@hackebrot.de>
  • Loading branch information
Trinaa and hackebrot authored Jul 15, 2022
1 parent 50381f7 commit 0b9c0f7
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 7 deletions.
2 changes: 1 addition & 1 deletion test-engineering/contract-tests/partner/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ USER app

EXPOSE ${PORT}
ENTRYPOINT [ "./entrypoint.sh" ]
CMD ["gunicorn","-c", "config/gunicorn_conf.py", "-k", "uvicorn.workers.UvicornWorker", "main:app"]
CMD ["gunicorn","-c", "config/gunicorn_conf.py", "--preload", "-k", "uvicorn.workers.UvicornWorker", "main:app"]
134 changes: 130 additions & 4 deletions test-engineering/contract-tests/partner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,144 @@ You can run the service using `docker compose` from the root directory:
docker compose run -p 5000:5000 partner
```

## Tiles API
## API

Example request:
Once the API service is running, API documentation can be found at
`http://0.0.0.0:5000/docs`.

### Records

**GET**: Endpoint to retrieve all historical Contile request records with a counter.

Example:

Request

```text
curl \
-X 'GET' \
-H 'accept: application/json' \
'http://0.0.0.0:5000/records/'
```

Response:

Code: `200`

Body:
```json
{
"records": [
{
"count": 1,
"record": {
"method": "GET",
"headers": [
{
"name": "host",
"value": "0.0.0.0:5000"
},
{
"name": "user-agent",
"value": "curl/7.79.1"
},
{
"name": "accept",
"value": "application/json"
}
],
"path": "/tilesp/desktop",
"query_parameters": [
{
"name": "partner",
"value": "demofeed"
},
{
"name": "sub1",
"value": "123456789"
},
{
"name": "sub2",
"value": "placement1"
},
{
"name": "country-code",
"value": "US"
},
{
"name": "region-code",
"value": "NY"
},
{
"name": "dma-code",
"value": "532"
},
{
"name": "form-factor",
"value": "desktop"
},
{
"name": "os-family",
"value": "macos"
},
{
"name": "v",
"value": "1.0"
},
{
"name": "out",
"value": "json"
},
{
"name": "results",
"value": "2"
}
]
}
}
]
}
```

**DELETE**: Endpoint to delete all historical Contile request records.

Example:

Request

```text
curl \
-X 'DELETE' \
-H 'accept: */*' \
'http://0.0.0.0:5000/records/'
```

Response

Code: `204`

Body: `N/A`

### Tiles

**GET**: Endpoint for requests from Contile.

Example:

Request

```text
curl \
-X 'GET' \
-H 'accept: application/json' \
'http://0.0.0.0:5000/tilesp?partner=demofeed&sub1=123456789&sub2=placement1&country-code=US&region-code=NY&form-factor=desktop&os-family=macos&v=1.0&out=json&results=2'
'http://0.0.0.0:5000/tilesp/desktop?partner=demofeed&sub1=123456789&sub2=placement1&country-code=US&region-code=NY&dma-code=532&form-factor=desktop&os-family=macos&v=1.0&out=json&results=2'
```

Example response body:
Response

Code: `200`

Body:

```json
{
Expand Down
27 changes: 26 additions & 1 deletion test-engineering/contract-tests/partner/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
import os
import pathlib
import sys
from multiprocessing import Manager
from typing import Dict, List

from fastapi import FastAPI, Query, Request, Response, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from models import Tiles
from models import Records, Tiles
from record_keeper import RecordKeeper
from responses import LoaderConfig, load_responses

logger = logging.getLogger("partner")
Expand All @@ -33,6 +35,12 @@

LOADER_CONFIG = LoaderConfig(RESPONSES_DIR)

# Object used for the synchronization of commonly used data types across processes.
multi_process_manager = Manager()

# Object used to manage recording of API calls
record_keeper = RecordKeeper(multi_process_manager)

app = FastAPI()

# This is only included for client errors such as invalid query parameter values
Expand Down Expand Up @@ -73,6 +81,20 @@ async def read_root():
return {"message": message}


@app.get("/records/", response_model=Records, status_code=200)
async def read_records():
"""Endpoint to retrieve all historical Contile request records with a counter."""

return record_keeper.get_all()


@app.delete("/records/", status_code=204)
async def delete_records():
"""Endpoint to delete all historical Contile request records."""

return record_keeper.clear()


# Make sure to update this when query parameters for `read_tilesp` change
ACCEPTED_QUERY_PARAMS = [
"partner",
Expand Down Expand Up @@ -140,6 +162,9 @@ async def read_tilesp(
):
"""Endpoint for requests from Contile."""

# record requests made by Contile for later verification by client
record_keeper.add(request)

unknown_query_params: List[str] = [
param for param in request.query_params if param not in ACCEPTED_QUERY_PARAMS
]
Expand Down
40 changes: 39 additions & 1 deletion test-engineering/contract-tests/partner/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from typing import Any, List, Union
from typing import Any, List, Tuple, Union

from pydantic import BaseModel

Expand All @@ -27,6 +27,9 @@ class Tiles(BaseModel):
class Header(BaseModel):
"""Model that represents a HTTP header."""

class Config:
frozen = True

name: str
value: str

Expand All @@ -38,3 +41,38 @@ class ResponseFromFile(BaseModel):
headers: List[Header]
content: Union[Tiles, Any]
delay: float = 0.0


class QueryParameter(BaseModel):
"""Model that represents a HTTP query parameter."""

class Config:
frozen = True

name: str
value: str


class Record(BaseModel):
"""Model that represents a request sent by Contile."""

class Config:
frozen = True

method: str
headers: Tuple[Header, ...]
path: str
query_parameters: Tuple[QueryParameter, ...]


class RecordCount(BaseModel):
"""Model that represents the number of times a request is sent by Contile."""

count: int
record: Record


class Records(BaseModel):
"""Model for a list of requests sent by Contile and their send count."""

records: List[RecordCount]
52 changes: 52 additions & 0 deletions test-engineering/contract-tests/partner/app/record_keeper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from collections import Counter
from multiprocessing.managers import SyncManager
from typing import List, Tuple

from fastapi import Request

from models import Header, QueryParameter, Record, RecordCount, Records


class RecordKeeper:
"""Responsible for Contile request history management"""

def __init__(self, multi_process_manager: SyncManager) -> None:
"""Create an instance of RecordKeeper."""

self._records: List[Record] = multi_process_manager.list()

def add(self, request: Request) -> None:
"""Create record from Fast API Request and add record to the record keeper."""

headers: Tuple[Header, ...] = tuple(
Header(name=name, value=value) for name, value in request.headers.items()
)

query_parameters: Tuple[QueryParameter, ...] = tuple(
QueryParameter(name=name, value=value)
for name, value in request.query_params.multi_items()
)

record: Record = Record(
method=request.method,
headers=headers,
path=request.url.path,
query_parameters=query_parameters,
)

self._records.append(record)

def clear(self) -> None:
"""Remove all records from the record keeper."""

self._records[:] = []

def get_all(self) -> Records:
"""Return all records in the record keeper with a counter."""

records: List[RecordCount] = [
RecordCount(count=count, record=record)
for record, count in Counter(self._records).items()
]

return Records(records=records)

0 comments on commit 0b9c0f7

Please sign in to comment.