Skip to content

Commit 15c6ca6

Browse files
authored
Merge pull request #12 from hakbailey/add-api-spec
Add initial API documentation and tests
2 parents c585836 + c5c3598 commit 15c6ca6

18 files changed

+2025
-124
lines changed

CONTRIBUTING.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ Hi there! We're excited to have you as a contributor.
1313
- [Set env variables for development](#set-env-variables-for-development)
1414
- [Configure postgres and run the dispatcher service](#configure-postgres-and-run-the-dispatcher-service)
1515
- [Configure and run the application](#configure-and-run-the-application)
16-
- [Development configuration](#development-configuration)
16+
- [Development Configuration](#development-configuration)
1717
- [Updating dependencies](#updating-dependencies)
1818
- [Running linters and code checks](#running-linters-and-code-checks)
1919
- [Running tests](#running-tests)
20+
- [Building API Documentation](#building-api-documentation)
2021

2122
## Things to know prior to submitting code
2223

@@ -97,7 +98,7 @@ AAP_USERNAME = "admin" # Username for AAP authentication
9798
AAP_PASSWORD = "password" # Password for AAP authentication
9899
```
99100

100-
*Note*: These defaults are placeholders for local development only. You must provide proper values for your environment by setting environment variables prefixed with `PATTERN_SERVICE_` or via a `.env` file.
101+
_Note_: These defaults are placeholders for local development only. You must provide proper values for your environment by setting environment variables prefixed with `PATTERN_SERVICE_` or via a `.env` file.
101102
For example:
102103

103104
```bash
@@ -136,3 +137,9 @@ Running the tests requires a postgres connection. The easiest way to do this is
136137
```bash
137138
make test
138139
```
140+
141+
## Building API Documentation
142+
143+
The pattern service includes support for generating an OpenAPI Description of the API. To build the documentation locally, run `make generate-api-spec`.
144+
145+
HTML-rendered API documentation can also be accessed within the running application at `http://localhost:8000/api/pattern-service/v1/docs/` or `http://localhost:8000/api/pattern-service/v1/docs/redoc/`

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ compose-up:
5050

5151
.PHONY: compose-down
5252
compose-down: ## Stop containers and remove containers, network, images and volumes created by compose-up
53-
$(COMPOSE_COMMAND) -f tools/podman/compose.yaml $(COMPOSE_OPTS) down --remove-orphans
53+
$(COMPOSE_COMMAND) -f tools/podman/compose.yaml $(COMPOSE_OPTS) down --remove-orphans --rmi
5454

5555
.PHONY: compose-restart
5656
compose-restart: compose-down compose-up ## Stop and remove existing infrastructure and start a new one
@@ -74,3 +74,11 @@ test: ## Run tests with a postgres database using docker-compose
7474
$(COMPOSE_COMMAND) -f tools/podman/compose-test.yaml $(COMPOSE_OPTS) up -d
7575
-tox -e test
7676
$(COMPOSE_COMMAND) -f tools/podman/compose-test.yaml $(COMPOSE_OPTS) down
77+
78+
# -------------------------------------
79+
# Docs
80+
# -------------------------------------
81+
82+
.PHONY: generate-api-spec
83+
generate-api-spec: ## Generate an OpenAPI specification
84+
python manage.py spectacular --validate --fail-on-warn --format openapi-json --file specifications/openapi.json

core/api_examples.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from drf_spectacular.utils import OpenApiExample
2+
3+
automation_get_response = OpenApiExample(
4+
"Sample automation GET response",
5+
value={
6+
"id": 1,
7+
"url": "/api/pattern-service/v1/automations/1/",
8+
"related": {"pattern_instance": "/api/pattern-service/v1/pattern_instances/1/"},
9+
"summary_fields": {"pattern_instance": {"id": 1}},
10+
"created": "2025-06-25T01:02:03Z",
11+
"created_by": None,
12+
"modified": "2025-06-25T01:02:03Z",
13+
"modified_by": None,
14+
"automation_type": "job_template",
15+
"automation_id": 12,
16+
"primary": True,
17+
"pattern_instance": 1,
18+
},
19+
response_only=True,
20+
)
21+
22+
controller_label_get_response = OpenApiExample(
23+
"Sample controller label GET response",
24+
value={
25+
"id": 1,
26+
"url": "/api/pattern-service/v1/controller_labels/1/",
27+
"related": {},
28+
"summary_fields": {},
29+
"created": "2025-06-25T01:02:03Z",
30+
"created_by": None,
31+
"modified": "2025-06-25T01:02:03Z",
32+
"modified_by": None,
33+
"label_id": 5,
34+
},
35+
response_only=True,
36+
)
37+
38+
pattern_get_response = OpenApiExample(
39+
"Sample pattern GET response",
40+
value={
41+
"id": 1,
42+
"url": "/api/pattern-service/v1/patterns/1/",
43+
"related": {},
44+
"summary_fields": {},
45+
"created": "2025-06-25T01:02:03Z",
46+
"created_by": None,
47+
"modified": "2025-06-25T01:02:03Z",
48+
"modified_by": None,
49+
"collection_name": "mynamespace.mycollection",
50+
"collection_version": "1.0.0",
51+
"collection_version_uri": None,
52+
"pattern_name": "mypattern",
53+
"pattern_definition": None,
54+
},
55+
response_only=True,
56+
)
57+
58+
pattern_post_request = OpenApiExample(
59+
"Sample pattern POST request",
60+
value={
61+
"collection_name": "mynamespace.mycollection",
62+
"collection_version": "1.0.0",
63+
"pattern_name": "mypattern",
64+
},
65+
request_only=True,
66+
)
67+
68+
pattern_post_response = OpenApiExample(
69+
"Sample pattern POST response",
70+
value={
71+
"message": "Pattern creation initiated. Check task status for progress.",
72+
"task_id": 1,
73+
},
74+
response_only=True,
75+
)
76+
77+
pattern_instance_get_response = OpenApiExample(
78+
"Sample pattern instance GET response",
79+
value={
80+
"id": 1,
81+
"url": "/api/pattern-service/v1/pattern_instances/1/",
82+
"related": {"pattern": "/api/pattern-service/v1/patterns/1/"},
83+
"summary_fields": {"pattern": {"id": 1}},
84+
"created": "2025-06-25T01:02:03Z",
85+
"created_by": None,
86+
"modified": "2025-06-25T01:02:03Z",
87+
"modified_by": None,
88+
"organization_id": 1,
89+
"controller_project_id": None,
90+
"controller_ee_id": None,
91+
"controller_labels": [1],
92+
"credentials": {"ee": 1, "project": 2},
93+
"executors": {
94+
"teams": [1, 2],
95+
"users": [1, 2, 3],
96+
},
97+
"pattern": 1,
98+
},
99+
response_only=True,
100+
)
101+
102+
pattern_instance_post_request = OpenApiExample(
103+
"Sample pattern instance POST request",
104+
value={
105+
"organization_id": 1,
106+
"credentials": {
107+
"ee": 1,
108+
"project": 2,
109+
},
110+
"executors": {
111+
"teams": [1, 2],
112+
"users": [1, 2, 3],
113+
},
114+
"pattern": 1,
115+
},
116+
request_only=True,
117+
)
118+
119+
pattern_instance_post_response = OpenApiExample(
120+
"Sample pattern instance POST response",
121+
value={
122+
"message": (
123+
"Pattern instance creation initiated. Check task status for progress."
124+
),
125+
"task_id": 1,
126+
},
127+
response_only=True,
128+
)
129+
130+
task_get_response = OpenApiExample(
131+
"Sample task GET response",
132+
value={
133+
"id": 1,
134+
"url": "/api/pattern-service/v1/tasks/1/",
135+
"related": {},
136+
"summary_fields": {},
137+
"created": "2025-06-25T01:02:03Z",
138+
"created_by": None,
139+
"modified": "2025-06-25T01:02:03Z",
140+
"modified_by": None,
141+
"status": "Running",
142+
"details": {"some": "data"},
143+
},
144+
response_only=True,
145+
)

core/tests/conftest.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import pytest
2+
from rest_framework.test import APIClient
3+
4+
from core import api_examples
5+
from core import models
6+
7+
8+
@pytest.fixture()
9+
def client():
10+
client = APIClient()
11+
return client
12+
13+
14+
@pytest.fixture()
15+
def automation(db, pattern_instance) -> models.Automation:
16+
automation = models.Automation.objects.create(
17+
automation_type=api_examples.automation_get_response.value["automation_type"],
18+
automation_id=api_examples.automation_get_response.value["automation_id"],
19+
pattern_instance=pattern_instance,
20+
primary=api_examples.automation_get_response.value["primary"],
21+
)
22+
return automation
23+
24+
25+
@pytest.fixture()
26+
def controller_label(db) -> models.ControllerLabel:
27+
controller_label = models.ControllerLabel.objects.create(
28+
label_id=api_examples.controller_label_get_response.value["label_id"]
29+
)
30+
return controller_label
31+
32+
33+
@pytest.fixture()
34+
def pattern(db) -> models.Pattern:
35+
pattern = models.Pattern.objects.create(
36+
collection_name=api_examples.pattern_post_request.value["collection_name"],
37+
collection_version=api_examples.pattern_post_request.value[
38+
"collection_version"
39+
],
40+
pattern_name=api_examples.pattern_post_request.value["pattern_name"],
41+
)
42+
return pattern
43+
44+
45+
@pytest.fixture()
46+
def pattern_instance(db, pattern) -> models.PatternInstance:
47+
pattern_instance = models.PatternInstance.objects.create(
48+
credentials=api_examples.pattern_instance_post_request.value["credentials"],
49+
executors=api_examples.pattern_instance_post_request.value["executors"],
50+
organization_id=1,
51+
pattern=pattern,
52+
)
53+
return pattern_instance
54+
55+
56+
@pytest.fixture()
57+
def task(db) -> models.Task:
58+
task = models.Task.objects.create(
59+
status=api_examples.task_get_response.value["status"],
60+
details=api_examples.task_get_response.value["details"],
61+
)
62+
return task

core/tests/test_api_v1.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import pytest
2+
from freezegun import freeze_time
3+
from rest_framework import status
4+
5+
from core import api_examples
6+
7+
8+
@pytest.fixture(autouse=True)
9+
def frozen_time():
10+
with freeze_time("2025-06-25 01:02:03"):
11+
yield
12+
13+
14+
def test_retrieve_automation_success(client, automation):
15+
url = f"/api/pattern-service/v1/automations/{automation.pk}/"
16+
response = client.get(url)
17+
assert response.status_code == status.HTTP_200_OK
18+
assert response.json() == api_examples.automation_get_response.value
19+
20+
21+
def test_list_automations_success(client, automation):
22+
url = "/api/pattern-service/v1/automations/"
23+
response = client.get(url)
24+
assert response.status_code == status.HTTP_200_OK
25+
assert response.json() == [api_examples.automation_get_response.value]
26+
27+
28+
def test_retrieve_controller_label_success(client, controller_label):
29+
url = f"/api/pattern-service/v1/controller_labels/{controller_label.pk}/"
30+
response = client.get(url)
31+
assert response.status_code == status.HTTP_200_OK
32+
assert response.json() == api_examples.controller_label_get_response.value
33+
34+
35+
def test_list_controller_labels_success(client, controller_label):
36+
url = "/api/pattern-service/v1/controller_labels/"
37+
response = client.get(url)
38+
assert response.status_code == status.HTTP_200_OK
39+
assert response.json() == [api_examples.controller_label_get_response.value]
40+
41+
42+
def test_create_pattern_success(client, db):
43+
url = "/api/pattern-service/v1/patterns/"
44+
data = api_examples.pattern_post_request.value
45+
response = client.post(url, data, format="json")
46+
assert response.status_code == status.HTTP_202_ACCEPTED
47+
assert response.json() == api_examples.pattern_post_response.value
48+
49+
50+
def test_retrieve_pattern_success(client, pattern):
51+
url = f"/api/pattern-service/v1/patterns/{pattern.pk}/"
52+
response = client.get(url)
53+
assert response.status_code == status.HTTP_200_OK
54+
assert response.json() == api_examples.pattern_get_response.value
55+
56+
57+
def test_list_patterns_success(client, pattern):
58+
url = "/api/pattern-service/v1/patterns/"
59+
response = client.get(url)
60+
assert response.status_code == status.HTTP_200_OK
61+
assert response.json() == [api_examples.pattern_get_response.value]
62+
63+
64+
def test_create_pattern_instance_success(client, pattern):
65+
url = "/api/pattern-service/v1/pattern_instances/"
66+
data = api_examples.pattern_instance_post_request.value
67+
data["pattern"] = pattern.pk
68+
response = client.post(url, data, format="json")
69+
assert response.status_code == status.HTTP_202_ACCEPTED
70+
assert response.json() == api_examples.pattern_instance_post_response.value
71+
72+
73+
def test_retrieve_pattern_instance_success(client, controller_label, pattern_instance):
74+
pattern_instance.controller_labels.add(controller_label)
75+
url = f"/api/pattern-service/v1/pattern_instances/{pattern_instance.pk}/"
76+
response = client.get(url)
77+
assert response.status_code == status.HTTP_200_OK
78+
assert response.json() == api_examples.pattern_instance_get_response.value
79+
80+
81+
def test_list_pattern_instances_success(client, controller_label, pattern_instance):
82+
pattern_instance.controller_labels.add(controller_label)
83+
url = "/api/pattern-service/v1/pattern_instances/"
84+
response = client.get(url)
85+
assert response.status_code == status.HTTP_200_OK
86+
assert response.json() == [api_examples.pattern_instance_get_response.value]
87+
88+
89+
def test_retrieve_task_success(client, task):
90+
url = f"/api/pattern-service/v1/tasks/{task.pk}/"
91+
response = client.get(url)
92+
assert response.status_code == status.HTTP_200_OK
93+
assert response.json() == api_examples.task_get_response.value
94+
95+
96+
def test_list_tasks_success(client, task):
97+
url = "/api/pattern-service/v1/tasks/"
98+
response = client.get(url)
99+
assert response.status_code == status.HTTP_200_OK
100+
assert response.json() == [api_examples.task_get_response.value]

0 commit comments

Comments
 (0)