Skip to content

Commit 8c102b5

Browse files
committed
Add initial API documentation and tests
1 parent 1211fed commit 8c102b5

File tree

14 files changed

+1911
-107
lines changed

14 files changed

+1911
-107
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_response = OpenApiExample(
4+
"Sample automation 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_response = OpenApiExample(
23+
"Sample controller label 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_post = OpenApiExample(
39+
"Sample pattern POST",
40+
value={
41+
"collection_name": "mynamespace.mycollection",
42+
"collection_version": "1.0.0",
43+
"pattern_name": "mypattern",
44+
},
45+
request_only=True,
46+
)
47+
48+
pattern_post_response = OpenApiExample(
49+
"Sample pattern POST response",
50+
value={
51+
"message": "Pattern creation initiated. Check task status for progress.",
52+
"task_id": 1,
53+
},
54+
response_only=True,
55+
)
56+
57+
pattern_response = OpenApiExample(
58+
"Sample pattern GET response",
59+
value={
60+
"id": 1,
61+
"url": "/api/pattern-service/v1/patterns/1/",
62+
"related": {},
63+
"summary_fields": {},
64+
"created": "2025-06-25T01:02:03Z",
65+
"created_by": None,
66+
"modified": "2025-06-25T01:02:03Z",
67+
"modified_by": None,
68+
"collection_name": "mynamespace.mycollection",
69+
"collection_version": "1.0.0",
70+
"collection_version_uri": None,
71+
"pattern_name": "mypattern",
72+
"pattern_definition": None,
73+
},
74+
response_only=True,
75+
)
76+
77+
pattern_instance_post = OpenApiExample(
78+
"Sample pattern instance POST",
79+
value={
80+
"organization_id": 1,
81+
"credentials": {
82+
"ee": 1,
83+
"project": 2,
84+
},
85+
"executors": {
86+
"teams": [1, 2],
87+
"users": [1, 2, 3],
88+
},
89+
"pattern": 1,
90+
},
91+
request_only=True,
92+
)
93+
94+
pattern_instance_post_response = OpenApiExample(
95+
"Sample pattern instance POST response",
96+
value={
97+
"message": (
98+
"Pattern instance creation initiated. Check task status for progress."
99+
),
100+
"task_id": 1,
101+
},
102+
response_only=True,
103+
)
104+
105+
pattern_instance_response = OpenApiExample(
106+
"Sample pattern instance response",
107+
value={
108+
"id": 1,
109+
"url": "/api/pattern-service/v1/pattern_instances/1/",
110+
"related": {"pattern": "/api/pattern-service/v1/patterns/1/"},
111+
"summary_fields": {"pattern": {"id": 1}},
112+
"created": "2025-06-25T01:02:03Z",
113+
"created_by": None,
114+
"modified": "2025-06-25T01:02:03Z",
115+
"modified_by": None,
116+
"organization_id": 1,
117+
"controller_project_id": None,
118+
"controller_ee_id": None,
119+
"controller_labels": [1],
120+
"credentials": {"ee": 1, "project": 2},
121+
"executors": {
122+
"teams": [1, 2],
123+
"users": [1, 2, 3],
124+
},
125+
"pattern": 1,
126+
},
127+
response_only=True,
128+
)
129+
130+
task_response = OpenApiExample(
131+
"Sample task 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/test_api_v1.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import pytest
2+
from freezegun import freeze_time
3+
from rest_framework import status
4+
from rest_framework.test import APIClient
5+
6+
from core import api_examples
7+
from core import models
8+
9+
10+
@pytest.fixture(autouse=True)
11+
def frozen_time():
12+
with freeze_time("2025-06-25 01:02:03"):
13+
yield
14+
15+
16+
@pytest.fixture()
17+
def client():
18+
client = APIClient()
19+
return client
20+
21+
22+
@pytest.fixture()
23+
def automation(db, pattern_instance) -> models.Automation:
24+
automation = models.Automation.objects.create(
25+
automation_type=api_examples.automation_response.value["automation_type"],
26+
automation_id=api_examples.automation_response.value["automation_id"],
27+
pattern_instance=pattern_instance,
28+
primary=api_examples.automation_response.value["primary"],
29+
)
30+
return automation
31+
32+
33+
@pytest.fixture()
34+
def controller_label(db) -> models.ControllerLabel:
35+
controller_label = models.ControllerLabel.objects.create(
36+
label_id=api_examples.controller_label_response.value["label_id"]
37+
)
38+
return controller_label
39+
40+
41+
@pytest.fixture()
42+
def pattern(db) -> models.Pattern:
43+
pattern = models.Pattern.objects.create(
44+
collection_name=api_examples.pattern_post.value["collection_name"],
45+
collection_version=api_examples.pattern_post.value["collection_version"],
46+
pattern_name=api_examples.pattern_post.value["pattern_name"],
47+
)
48+
return pattern
49+
50+
51+
@pytest.fixture()
52+
def pattern_instance(db, pattern) -> models.PatternInstance:
53+
pattern_instance = models.PatternInstance.objects.create(
54+
credentials=api_examples.pattern_instance_post.value["credentials"],
55+
executors=api_examples.pattern_instance_post.value["executors"],
56+
organization_id=1,
57+
pattern=pattern,
58+
)
59+
return pattern_instance
60+
61+
62+
@pytest.fixture()
63+
def task(db) -> models.Task:
64+
task = models.Task.objects.create(
65+
status=api_examples.task_response.value["status"],
66+
details=api_examples.task_response.value["details"],
67+
)
68+
return task
69+
70+
71+
def test_retrieve_automation_success(client, automation):
72+
url = f"/api/pattern-service/v1/automations/{automation.pk}/"
73+
response = client.get(url)
74+
assert response.status_code == status.HTTP_200_OK
75+
assert response.json() == api_examples.automation_response.value
76+
77+
78+
def test_list_automations_success(client, automation):
79+
url = "/api/pattern-service/v1/automations/"
80+
response = client.get(url)
81+
assert response.status_code == status.HTTP_200_OK
82+
assert response.json() == [api_examples.automation_response.value]
83+
84+
85+
def test_retrieve_controller_label_success(client, controller_label):
86+
url = f"/api/pattern-service/v1/controller_labels/{controller_label.pk}/"
87+
response = client.get(url)
88+
assert response.status_code == status.HTTP_200_OK
89+
assert response.json() == api_examples.controller_label_response.value
90+
91+
92+
def test_list_controller_labels_success(client, controller_label):
93+
url = "/api/pattern-service/v1/controller_labels/"
94+
response = client.get(url)
95+
assert response.status_code == status.HTTP_200_OK
96+
assert response.json() == [api_examples.controller_label_response.value]
97+
98+
99+
def test_create_pattern_success(client, db):
100+
url = "/api/pattern-service/v1/patterns/"
101+
data = api_examples.pattern_post.value
102+
response = client.post(url, data, format="json")
103+
assert response.status_code == status.HTTP_202_ACCEPTED
104+
assert response.json() == api_examples.pattern_post_response.value
105+
106+
107+
def test_retrieve_pattern_success(client, pattern):
108+
url = f"/api/pattern-service/v1/patterns/{pattern.pk}/"
109+
response = client.get(url)
110+
assert response.status_code == status.HTTP_200_OK
111+
assert response.json() == api_examples.pattern_response.value
112+
113+
114+
def test_list_patterns_success(client, pattern):
115+
url = "/api/pattern-service/v1/patterns/"
116+
response = client.get(url)
117+
assert response.status_code == status.HTTP_200_OK
118+
assert response.json() == [api_examples.pattern_response.value]
119+
120+
121+
def test_create_pattern_instance_success(client, pattern):
122+
url = "/api/pattern-service/v1/pattern_instances/"
123+
data = api_examples.pattern_instance_post.value
124+
data["pattern"] = pattern.pk
125+
response = client.post(url, data, format="json")
126+
assert response.status_code == status.HTTP_202_ACCEPTED
127+
assert response.json() == api_examples.pattern_instance_post_response.value
128+
129+
130+
def test_retrieve_pattern_instance_success(client, controller_label, pattern_instance):
131+
pattern_instance.controller_labels.add(controller_label)
132+
url = f"/api/pattern-service/v1/pattern_instances/{pattern_instance.pk}/"
133+
response = client.get(url)
134+
assert response.status_code == status.HTTP_200_OK
135+
assert response.json() == api_examples.pattern_instance_response.value
136+
137+
138+
def test_list_pattern_instances_success(client, controller_label, pattern_instance):
139+
pattern_instance.controller_labels.add(controller_label)
140+
url = "/api/pattern-service/v1/pattern_instances/"
141+
response = client.get(url)
142+
assert response.status_code == status.HTTP_200_OK
143+
assert response.json() == [api_examples.pattern_instance_response.value]
144+
145+
146+
def test_retrieve_task_success(client, task):
147+
url = f"/api/pattern-service/v1/tasks/{task.pk}/"
148+
response = client.get(url)
149+
assert response.status_code == status.HTTP_200_OK
150+
assert response.json() == api_examples.task_response.value
151+
152+
153+
def test_list_tasks_success(client, task):
154+
url = "/api/pattern-service/v1/tasks/"
155+
response = client.get(url)
156+
assert response.status_code == status.HTTP_200_OK
157+
assert response.json() == [api_examples.task_response.value]

0 commit comments

Comments
 (0)