Skip to content

WIP: Add initial API documentation and tests #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ Hi there! We're excited to have you as a contributor.
- [pattern-service](#pattern-service)
- [Table of contents](#table-of-contents)
- [Things to know prior to submitting code](#things-to-know-prior-to-submitting-code)
- [Build and Run the Development Environment](#build-and-run-the-development-environment)
- [Clone the repo](#clone-the-repo)
- [Configure python environment](#configure-python-environment)
- [Configure and run the application](#configure-and-run-the-application)
- [Build and Run the Development Environment](#build-and-run-the-development-environment)
- [Clone the repo](#clone-the-repo)
- [Configure python environment](#configure-python-environment)
- [Configure and run the application](#configure-and-run-the-application)
- [Testing](#testing)
- [Running tests locally](#running-tests-locally)
- [Building API Documentation](#building-api-documentation)

## Things to know prior to submitting code

Expand All @@ -19,15 +22,15 @@ Hi there! We're excited to have you as a contributor.
- If collaborating with someone else on the same branch, consider using `--force-with-lease` instead of `--force`. This will prevent you from accidentally overwriting commits pushed by someone else. For more information, see [git push docs](https://git-scm.com/docs/git-push#git-push---force-with-leaseltrefnamegt).
- We ask all of our community members and contributors to adhere to the [Ansible code of conduct](http://docs.ansible.com/ansible/latest/community/code_of_conduct.html). If you have questions, or need assistance, please reach out to our community team at [[email protected]](mailto:[email protected])

### Build and Run the Development Environment
## Build and Run the Development Environment

#### Clone the repo
### Clone the repo

If you have not already done so, you will need to clone, or create a local copy, of the [pattern-service repository](https://github.com/ansible/pattern-service).
For more on how to clone the repo, view [git clone help](https://git-scm.com/docs/git-clone).
Once you have a local copy, run the commands in the following sections from the root of the project tree.

#### Configure python environment
### Configure python environment

Create python virtual environment using one of the below commands:

Expand All @@ -41,8 +44,22 @@ Install required python modules

`pip install -r ./requirements-all.txt`

#### Configure and run the application
### Configure and run the application

`python manage.py migrate && python manage.py runserver`

The application can be reached in your browser at `https://localhost:8000/`
The application can be reached in your browser at `http://localhost:8000/`

## Testing

All code contributions should include unit tests. Functional and integration tests may also be required for some types of code changes.

### Running tests locally

This repository uses pytest to run its test suite. To run the tests locally, run `make test`.

## Building API Documentation

The pattern service includes support for generating an OpenAPI Description of the API. To build the documentation locally, run `make generate-api-spec`.

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/`
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.PHONY: build build-multi run test clean install-deps lint push-quay login-quay push-quay-multi
.PHONY: build build-multi run test clean install-deps generate-api-spec lint push-quay login-quay push-quay-multi requirements


# Image name and tag
CONTAINER_RUNTIME ?= podman
Expand All @@ -25,3 +26,13 @@ push: ensure-namespace build
@echo "Tagging and pushing to registry..."
$(CONTAINER_RUNTIME) tag $(IMAGE_NAME):$(IMAGE_TAG) quay.io/$(QUAY_NAMESPACE)/$(IMAGE_NAME):$(IMAGE_TAG)
$(CONTAINER_RUNTIME) push quay.io/$(QUAY_NAMESPACE)/$(IMAGE_NAME):$(IMAGE_TAG)

requirements:
pip-compile -o requirements/requirements.txt requirements/requirements.in
pip-compile -o requirements/requirements-dev.txt requirements/requirements.in requirements/requirements-dev.in

generate-api-spec:
python manage.py spectacular --validate --fail-on-warn --format openapi-json --file specifications/openapi.json

test:
pytest -vv
130 changes: 130 additions & 0 deletions core/tests/test_api_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import pytest
from freezegun import freeze_time
from rest_framework import status
from rest_framework.test import APIClient

from core import models
from docs import examples


@pytest.fixture(autouse=True)
def frozen_time():
with freeze_time("2025-06-25 01:02:03"):
yield


@pytest.fixture()
def client():
client = APIClient()
return client


@pytest.fixture
def automation(db, pattern_instance) -> models.Automation:
automation = models.Automation.objects.create(
automation_type=examples.automation_response.value["automation_type"],
automation_id=examples.automation_response.value["automation_id"],
pattern_instance=pattern_instance,
primary=examples.automation_response.value["primary"],
)
return automation


@pytest.fixture
def controller_label(db) -> models.ControllerLabel:
controller_label = models.ControllerLabel.objects.create(label_id=examples.controller_label_response.value["label_id"])
return controller_label


@pytest.fixture
def pattern(db) -> models.Pattern:
pattern = models.Pattern.objects.create(
collection_name=examples.pattern_post.value["collection_name"],
collection_version=examples.pattern_post.value["collection_version"],
pattern_name=examples.pattern_post.value["pattern_name"],
)
return pattern


@pytest.fixture
def pattern_instance(db, pattern) -> models.PatternInstance:
pattern_instance = models.PatternInstance.objects.create(
credentials=examples.pattern_instance_post.value["credentials"],
executors=examples.pattern_instance_post.value["executors"],
organization_id=1,
pattern=pattern,
)
return pattern_instance


def test_retrieve_automation_success(db, client, automation):
url = f"/api/pattern-service/v1/automations/{automation.pk}/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == examples.automation_response.value


def test_list_automations_success(db, client, automation):
url = "/api/pattern-service/v1/automations/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == [examples.automation_response.value]


def test_retrieve_controller_label_success(db, client, controller_label):
url = f"/api/pattern-service/v1/controller_labels/{controller_label.pk}/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == examples.controller_label_response.value


def test_list_controller_labels_success(db, client, controller_label):
url = "/api/pattern-service/v1/controller_labels/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == [examples.controller_label_response.value]


def test_create_pattern_success(db, client):
url = "/api/pattern-service/v1/patterns/"
data = examples.pattern_post.value
response = client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert response.json() == examples.pattern_response.value


def test_retrieve_pattern_success(db, client, pattern):
url = f"/api/pattern-service/v1/patterns/{pattern.pk}/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == examples.pattern_response.value


def test_list_patterns_success(db, client, pattern):
url = "/api/pattern-service/v1/patterns/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == [examples.pattern_response.value]


def test_create_pattern_instance_success(db, client, pattern):
url = "/api/pattern-service/v1/pattern_instances/"
data = examples.pattern_instance_post.value
data["pattern"] = pattern.pk
response = client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert response.json() == examples.pattern_instance_response.value


def test_retrieve_pattern_instance_success(db, client, pattern_instance):
url = f"/api/pattern-service/v1/pattern_instances/{pattern_instance.pk}/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == examples.pattern_instance_response.value


def test_list_pattern_instances_success(db, client, pattern_instance):
url = "/api/pattern-service/v1/pattern_instances/"
response = client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.json() == [examples.pattern_instance_response.value]
4 changes: 2 additions & 2 deletions core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

router = AssociationResourceRouter()
router.register(r'patterns', PatternViewSet, basename='pattern')
router.register(r'controllerlabels', ControllerLabelViewSet, basename='controllerlabel')
router.register(r'patterninstances', PatternInstanceViewSet, basename='patterninstance')
router.register(r'controller_labels', ControllerLabelViewSet, basename='controller_label')
router.register(r'pattern_instances', PatternInstanceViewSet, basename='pattern_instance')
router.register(r'automations', AutomationViewSet, basename='automation')

urlpatterns = router.urls
78 changes: 68 additions & 10 deletions core/views.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,105 @@
from ansible_base.lib.utils.views.ansible_base import AnsibleBaseView
from drf_spectacular.utils import extend_schema
from drf_spectacular.utils import extend_schema_view
from rest_framework.decorators import api_view
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

from .models import Automation
from .models import ControllerLabel
from .models import Pattern
from .models import PatternInstance
from .serializers import AutomationSerializer
from .serializers import ControllerLabelSerializer
from .serializers import PatternInstanceSerializer
from .serializers import PatternSerializer
from core.models import Automation
from core.models import ControllerLabel
from core.models import Pattern
from core.models import PatternInstance
from core.serializers import AutomationSerializer
from core.serializers import ControllerLabelSerializer
from core.serializers import PatternInstanceSerializer
from core.serializers import PatternSerializer
from docs import examples


class CoreViewSet(AnsibleBaseView):
pass


@extend_schema_view(
create=extend_schema(
description="Add an Ansible pattern to the service.",
examples=[examples.pattern_post, examples.pattern_response],
),
list=extend_schema(
description="Retrieve information about all Ansible pattern added to the service.",
examples=[examples.pattern_response],
),
retrieve=extend_schema(
description="Retrieve information about a single Ansible pattern by ID.",
examples=[examples.pattern_response],
),
)
class PatternViewSet(CoreViewSet, ModelViewSet):
http_method_names = ["get", "post", "delete", "head", "options"]
queryset = Pattern.objects.all()
serializer_class = PatternSerializer


@extend_schema_view(
list=extend_schema(
description="Retrieve information about all controller labels created by the service.",
examples=[examples.automation_response],
),
retrieve=extend_schema(
description="Retrieve information about a single controller label by ID.",
examples=[examples.automation_response],
),
)
class ControllerLabelViewSet(CoreViewSet, ModelViewSet):
http_method_names = ["get", "delete", "head", "options"]
queryset = ControllerLabel.objects.all()
serializer_class = ControllerLabelSerializer


@extend_schema_view(
create=extend_schema(
description="Create an instance of an Ansible pattern, creating its defined AAP resources and saving their IDs.",
examples=[examples.pattern_instance_post, examples.pattern_instance_response],
),
list=extend_schema(
description="Retrieve information about all pattern instances.",
examples=[examples.pattern_instance_response],
),
retrieve=extend_schema(
description="Retrieve information about a single pattern instance by ID.",
examples=[examples.pattern_instance_response],
),
)
class PatternInstanceViewSet(CoreViewSet, ModelViewSet):
http_method_names = ["get", "post", "delete", "head", "options"]
queryset = PatternInstance.objects.all()
serializer_class = PatternInstanceSerializer


@extend_schema_view(
list=extend_schema(
description="Retrieve information about all automations created by the service.",
examples=[examples.automation_response],
),
retrieve=extend_schema(
description="Retrieve information about a single automation by ID.",
examples=[examples.automation_response],
),
)
class AutomationViewSet(CoreViewSet, ModelViewSet):
http_method_names = ["get", "delete", "head", "options"]
queryset = Automation.objects.all()
serializer_class = AutomationSerializer


@extend_schema(exclude=True)
@api_view(["GET"])
def ping(request):
def ping(request: Request) -> Response:
return Response(data={"status": "ok"}, status=200)


@extend_schema(exclude=True)
@api_view(["GET"])
def test(request):
def test(request: Request) -> Response:
return Response(data={"hello": "world"}, status=200)
Loading