Skip to content

Commit 3ae2c84

Browse files
authored
MPT-20436: Added endpoints and e2e tests for program certificates (#319)
This pull request adds support for program certificates in the API client, including synchronous and asynchronous service classes and comprehensive end-to-end and unit test coverage. **Program Certificate Service Implementation:** - Added `Certificate` model and `CertificateService` / `AsyncCertificateService` classes in `mpt_api_client/resources/program/certificates.py`, providing CRUD operations via `ManagedResourceMixin` and `CollectionMixin`, along with a `terminate` action method for both sync and async services. **Integration with Program Module:** - Registered `certificates` as a property on `Program` and `AsyncProgram` in `mpt_api_client/resources/program/program.py`, exposing the new service alongside the existing enrollments service. **Testing Enhancements:** - Added end-to-end tests for both sync and async certificate flows in `tests/e2e/program/certificate/test_sync_certificate.py` and `test_async_certificate.py`, covering creation, retrieval, listing, updating, termination, and error handling. - Created test fixtures for certificate data in `tests/e2e/program/certificate/conftest.py`. - Added unit tests for `CertificateService`, `AsyncCertificateService`, and the `Program` / `AsyncProgram` integration in `tests/unit/resources/program/test_certificates.py` and `tests/unit/resources/program/test_program.py`. **Configuration Updates:** - Updated `e2e_config.test.json` with `program.certificate.id` for E2E testing. Closes [MPT-20436](https://softwareone.atlassian.net/browse/MPT-20436) [MPT-20436]: https://softwareone.atlassian.net/browse/MPT-20436?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2 parents 973253f + 2f156fc commit 3ae2c84

8 files changed

Lines changed: 447 additions & 0 deletions

File tree

e2e_config.test.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
"notifications.subscriber.id": "NTS-0829-7123-7123",
7171
"integration.extension.id": "EXT-6587-4477",
7272
"integration.term.id": "ETC-6587-4477-0062",
73+
"program.certificate.id": "CER-9646-2171-8417",
7374
"program.document.file.id": "PDM-9643-3741-0001",
7475
"program.enrollment.assignee.id": "USR-6337-1324",
7576
"program.enrollment.attachment.id": "ENA-3965-5056-7966-0001",
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.mixins import (
3+
AsyncCollectionMixin,
4+
AsyncManagedResourceMixin,
5+
CollectionMixin,
6+
ManagedResourceMixin,
7+
)
8+
from mpt_api_client.models import Model
9+
from mpt_api_client.models.model import BaseModel, ResourceData
10+
11+
12+
class Certificate(Model):
13+
"""Program certificate resource.
14+
15+
Attributes:
16+
name: Certificate name.
17+
program: Reference to the program.
18+
vendor: Reference to the vendor.
19+
external_ids: External identifiers.
20+
client: Reference to the client.
21+
applicable_to: Applicable to which entities.
22+
licensee: Reference to the licensee.
23+
eligibility: Eligibility criteria.
24+
status: Certificate status.
25+
status_notes: Additional notes on the certificate status.
26+
parameters: Certificate parameters.
27+
audit: Audit information.
28+
"""
29+
30+
name: str | None
31+
program: BaseModel | None
32+
vendor: BaseModel | None
33+
external_ids: BaseModel | None
34+
client: BaseModel | None
35+
applicable_to: str | None
36+
licensee: BaseModel | None
37+
eligibility: BaseModel | None
38+
status: str | None
39+
status_notes: str | None
40+
parameters: BaseModel | None # noqa: WPS110
41+
audit: BaseModel | None
42+
43+
44+
class CertificateServiceConfig:
45+
"""Program certificate service config."""
46+
47+
_endpoint = "/public/v1/program/certificates"
48+
_model_class = Certificate
49+
_collection_key = "data"
50+
51+
52+
class CertificateService(
53+
ManagedResourceMixin[Certificate],
54+
CollectionMixin[Certificate],
55+
Service[Certificate],
56+
CertificateServiceConfig,
57+
):
58+
"""Program certificate service."""
59+
60+
def terminate(self, resource_id: str, resource_data: ResourceData | None = None) -> Certificate:
61+
"""Terminate a certificate.
62+
63+
Args:
64+
resource_id: Certificate ID.
65+
resource_data: Additional data for termination.
66+
67+
Returns:
68+
Terminated certificate.
69+
"""
70+
return self._resource(resource_id).post("/terminate", json=resource_data)
71+
72+
73+
class AsyncCertificateService(
74+
AsyncManagedResourceMixin[Certificate],
75+
AsyncCollectionMixin[Certificate],
76+
AsyncService[Certificate],
77+
CertificateServiceConfig,
78+
):
79+
"""Asynchronous program certificate service."""
80+
81+
async def terminate(
82+
self, resource_id: str, resource_data: ResourceData | None = None
83+
) -> Certificate:
84+
"""Asynchronously terminate a certificate.
85+
86+
Args:
87+
resource_id: Certificate ID.
88+
resource_data: Additional data for termination.
89+
90+
Returns:
91+
Terminated certificate.
92+
"""
93+
return await self._resource(resource_id).post("/terminate", json=resource_data)

mpt_api_client/resources/program/program.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
from mpt_api_client.http import AsyncHTTPClient, HTTPClient
2+
from mpt_api_client.resources.program.certificates import (
3+
AsyncCertificateService,
4+
CertificateService,
5+
)
26
from mpt_api_client.resources.program.enrollments import AsyncEnrollmentService, EnrollmentService
37
from mpt_api_client.resources.program.programs import AsyncProgramsService, ProgramsService
48

@@ -19,6 +23,11 @@ def enrollments(self) -> EnrollmentService:
1923
"""Enrollments service."""
2024
return EnrollmentService(http_client=self.http_client)
2125

26+
@property
27+
def certificates(self) -> CertificateService:
28+
"""Certificates service."""
29+
return CertificateService(http_client=self.http_client)
30+
2231

2332
class AsyncProgram:
2433
"""Program MPT API Module."""
@@ -35,3 +44,8 @@ def programs(self) -> AsyncProgramsService:
3544
def enrollments(self) -> AsyncEnrollmentService:
3645
"""Enrollments service."""
3746
return AsyncEnrollmentService(http_client=self.http_client)
47+
48+
@property
49+
def certificates(self) -> AsyncCertificateService:
50+
"""Certificates service."""
51+
return AsyncCertificateService(http_client=self.http_client)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def certificate_id(e2e_config):
6+
return e2e_config["program.certificate.id"]
7+
8+
9+
@pytest.fixture
10+
def invalid_certificate_id():
11+
return "CER-0000-0000-0000"
12+
13+
14+
@pytest.fixture
15+
def certificate_data(program_id, licensee_id, buyer_account_id):
16+
return {
17+
"name": "E2E Created Program Certificate",
18+
"program": {"id": program_id},
19+
"licensee": {"id": licensee_id},
20+
"client": {"id": buyer_account_id},
21+
"parameters": {"ordering": [], "fulfillment": []},
22+
}
23+
24+
25+
@pytest.fixture
26+
def terminated_certificate_data_factory():
27+
def factory(certificate_id: str):
28+
return {
29+
"id": certificate_id,
30+
"status": "terminated",
31+
"statusNotes": {
32+
"message": "Terminating certificate for E2E test",
33+
},
34+
}
35+
36+
return factory
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from mpt_api_client.rql.query_builder import RQLQuery
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
@pytest.fixture
10+
async def created_certificate(
11+
async_mpt_ops, async_mpt_vendor, certificate_data, terminated_certificate_data_factory
12+
):
13+
certificate = await async_mpt_ops.program.certificates.create(certificate_data)
14+
terminated_certificate_data = terminated_certificate_data_factory(certificate.id)
15+
16+
yield certificate
17+
18+
try:
19+
await async_mpt_vendor.program.certificates.terminate(
20+
certificate.id, terminated_certificate_data
21+
)
22+
except MPTAPIError as error:
23+
print(f"TEARDOWN - Unable to terminate certificate {certificate.id}: {error.title}") # noqa: WPS421
24+
25+
26+
async def test_get_certificate_by_id(async_mpt_vendor, certificate_id):
27+
result = await async_mpt_vendor.program.certificates.get(certificate_id)
28+
29+
assert result is not None
30+
31+
32+
async def test_get_certificate_by_invalid_id(async_mpt_vendor, invalid_certificate_id):
33+
with pytest.raises(MPTAPIError):
34+
await async_mpt_vendor.program.certificates.get(invalid_certificate_id)
35+
36+
37+
async def test_list_certificates(async_mpt_vendor):
38+
limit = 10
39+
40+
result = await async_mpt_vendor.program.certificates.fetch_page(limit=limit)
41+
42+
assert len(result) > 0
43+
44+
45+
async def test_filter_certificates(async_mpt_vendor, certificate_id):
46+
select_fields = ["-audit", "-parameters"]
47+
filtered_certificates = (
48+
async_mpt_vendor.program.certificates
49+
.filter(RQLQuery(id=certificate_id))
50+
.filter(RQLQuery(status="Active"))
51+
.select(*select_fields)
52+
)
53+
54+
result = [certificate async for certificate in filtered_certificates.iterate()]
55+
56+
assert len(result) == 1
57+
58+
59+
def test_create_certificate(created_certificate):
60+
result = created_certificate
61+
62+
assert result is not None
63+
64+
65+
async def test_update_certificate(async_mpt_client, created_certificate):
66+
updated_name = "E2E Updated Certificate Name"
67+
update_data = {"id": created_certificate.id, "name": updated_name}
68+
69+
result = await async_mpt_client.program.certificates.update(created_certificate.id, update_data)
70+
71+
assert result is not None
72+
73+
74+
async def test_terminate_certificate(
75+
async_mpt_vendor, created_certificate, terminated_certificate_data_factory
76+
):
77+
terminated_certificate_data = terminated_certificate_data_factory(created_certificate.id)
78+
79+
result = await async_mpt_vendor.program.certificates.terminate(
80+
created_certificate.id, terminated_certificate_data
81+
)
82+
83+
assert result is not None
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from mpt_api_client.rql.query_builder import RQLQuery
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
@pytest.fixture
10+
def created_certificate(mpt_ops, mpt_vendor, certificate_data, terminated_certificate_data_factory):
11+
certificate = mpt_ops.program.certificates.create(certificate_data)
12+
terminated_certificate_data = terminated_certificate_data_factory(certificate.id)
13+
14+
yield certificate
15+
16+
try:
17+
mpt_vendor.program.certificates.terminate(certificate.id, terminated_certificate_data)
18+
except MPTAPIError as error:
19+
print(f"TEARDOWN - Unable to terminate certificate {certificate.id}: {error.title}") # noqa: WPS421
20+
21+
22+
def test_get_certificate_by_id(mpt_vendor, certificate_id):
23+
result = mpt_vendor.program.certificates.get(certificate_id)
24+
25+
assert result is not None
26+
27+
28+
def test_get_certificate_by_invalid_id(mpt_vendor, invalid_certificate_id):
29+
with pytest.raises(MPTAPIError):
30+
mpt_vendor.program.certificates.get(invalid_certificate_id)
31+
32+
33+
def test_list_certificates(mpt_vendor):
34+
limit = 10
35+
36+
result = mpt_vendor.program.certificates.fetch_page(limit=limit)
37+
38+
assert len(result) > 0
39+
40+
41+
def test_filter_certificates(mpt_vendor, certificate_id):
42+
select_fields = ["-audit", "-parameters"]
43+
filtered_certificates = (
44+
mpt_vendor.program.certificates
45+
.filter(RQLQuery(id=certificate_id))
46+
.filter(RQLQuery(status="Active"))
47+
.select(*select_fields)
48+
)
49+
50+
result = list(filtered_certificates.iterate())
51+
52+
assert len(result) == 1
53+
54+
55+
def test_create_certificate(created_certificate):
56+
result = created_certificate
57+
58+
assert result is not None
59+
60+
61+
def test_update_certificate(mpt_client, created_certificate):
62+
updated_name = "E2E Updated Certificate Name"
63+
update_data = {"id": created_certificate.id, "name": updated_name}
64+
65+
result = mpt_client.program.certificates.update(created_certificate.id, update_data)
66+
67+
assert result is not None
68+
69+
70+
def test_terminate_certificate(
71+
mpt_vendor, created_certificate, terminated_certificate_data_factory
72+
):
73+
terminated_certificate_data = terminated_certificate_data_factory(created_certificate.id)
74+
75+
result = mpt_vendor.program.certificates.terminate(
76+
created_certificate.id, terminated_certificate_data
77+
)
78+
79+
assert result is not None

0 commit comments

Comments
 (0)