Skip to content

Commit 6197a2a

Browse files
jentykclaude
andcommitted
test(e2e): add notifications directories, footers and webhooks e2e coverage
Cover the mixin surface each new service exposes against the live TEST environment. Directories are read-only, so the tests cover get, 404 on a bogus id and the filtered iterate on a stable vendor-account directory registered as notifications.directory.id. Footers get the managed lifecycle through the operations client and webhooks the managed lifecycle plus enable and disable through the vendor client on the seeded product. Both platforms enforce a uniqueness key on create: one active footer per language and account, one webhook per type and bound object. The fixtures therefore treat the create call as the claim on that key: they try a list of supported candidates in order and move on when the platform reports the key as taken, so concurrent runs sharing the operations account or the seeded product cannot both win the same value. Footers soft-delete, which the delete test asserts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent fefaeed commit 6197a2a

14 files changed

Lines changed: 474 additions & 0 deletions

‎e2e_config.test.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"notifications.batch.id": "MST-3638-2460-4825",
6868
"notifications.category.id": "NTC-6157-0397",
6969
"notifications.contact.id": "CTT-0001-9158",
70+
"notifications.directory.id": "DIR-9614-2985",
7071
"notifications.message.id": "MSG-0000-6215-1019-0139",
7172
"notifications.subscriber.id": "NTS-0829-7123-7123",
7273
"integration.extension.id": "EXT-6587-4477",

‎tests/e2e/helper.py‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,62 @@ def create_fixture_resource_and_delete(service, resource_data, upload_file=None)
5858
_delete_resource(service, resource)
5959

6060

61+
async def _async_create_first_free(service, candidates, is_taken):
62+
remaining = list(candidates)
63+
while remaining:
64+
candidate = remaining.pop(0)
65+
try:
66+
return await service.create(candidate)
67+
except MPTAPIError as error:
68+
if not is_taken(error):
69+
raise
70+
raise RuntimeError(f"Every candidate payload is already taken: {candidates}")
71+
72+
73+
def _create_first_free(service, candidates, is_taken):
74+
for candidate in candidates:
75+
try:
76+
return service.create(candidate)
77+
except MPTAPIError as error:
78+
if not is_taken(error):
79+
raise
80+
raise RuntimeError(f"Every candidate payload is already taken: {candidates}")
81+
82+
83+
@asynccontextmanager
84+
async def async_create_first_free_fixture_and_delete(service, candidates, is_taken):
85+
"""Create the first candidate the platform accepts, then delete it on teardown.
86+
87+
``candidates`` are payloads that differ only in a server-enforced uniqueness key and
88+
``is_taken(error)`` tells whether a rejected create means that key is already in use.
89+
The create call itself is the claim, so two runs that race for the same key cannot both
90+
succeed and the loser moves on to the next candidate instead of failing.
91+
"""
92+
resource = await _async_create_first_free(service, candidates, is_taken)
93+
94+
try:
95+
yield resource
96+
finally:
97+
await _delete_async_resource(service, resource)
98+
99+
100+
@contextmanager
101+
def create_first_free_fixture_and_delete(service, candidates, is_taken):
102+
"""Create the first candidate the platform accepts, then delete it on teardown.
103+
104+
``candidates`` are payloads that differ only in a server-enforced uniqueness key and
105+
``is_taken(error)`` tells whether a rejected create means that key is already in use.
106+
The create call itself is the claim, so two runs that race for the same key cannot both
107+
succeed and the loser moves on to the next candidate instead of failing.
108+
"""
109+
resource = _create_first_free(service, candidates, is_taken)
110+
111+
try:
112+
yield resource
113+
finally:
114+
_delete_resource(service, resource)
115+
116+
61117
@asynccontextmanager
62118
async def async_create_fixture_resource_and_finalize(service, resource_data, finalize, logger):
63119
"""Create a resource, then transition it to a terminal state on teardown.

‎tests/e2e/notifications/directories/__init__.py‎

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pytest
2+
3+
4+
@pytest.fixture
5+
def directories_service(mpt_ops):
6+
return mpt_ops.notifications.directories
7+
8+
9+
@pytest.fixture
10+
def async_directories_service(async_mpt_ops):
11+
return async_mpt_ops.notifications.directories
12+
13+
14+
@pytest.fixture(scope="session")
15+
def directory_id(e2e_config):
16+
return e2e_config["notifications.directory.id"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from tests.e2e.helper import assert_async_service_filter_with_iterate
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
async def test_get_directory(async_directories_service, directory_id):
10+
result = await async_directories_service.get(directory_id)
11+
12+
assert result.id == directory_id
13+
14+
15+
async def test_get_directory_not_found(async_directories_service):
16+
bogus_id = "DIR-0000-0000"
17+
18+
with pytest.raises(MPTAPIError, match=r"404 Not Found"):
19+
await async_directories_service.get(bogus_id)
20+
21+
22+
async def test_filter_directories(async_directories_service, directory_id):
23+
await assert_async_service_filter_with_iterate(
24+
async_directories_service, directory_id, None
25+
) # act
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from tests.e2e.helper import assert_service_filter_with_iterate
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
def test_get_directory(directories_service, directory_id):
10+
result = directories_service.get(directory_id)
11+
12+
assert result.id == directory_id
13+
14+
15+
def test_get_directory_not_found(directories_service):
16+
bogus_id = "DIR-0000-0000"
17+
18+
with pytest.raises(MPTAPIError, match=r"404 Not Found"):
19+
directories_service.get(bogus_id)
20+
21+
22+
def test_filter_directories(directories_service, directory_id):
23+
assert_service_filter_with_iterate(directories_service, directory_id, None) # act

‎tests/e2e/notifications/footers/__init__.py‎

Whitespace-only changes.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import pytest
2+
3+
from tests.e2e.helper import (
4+
async_create_first_free_fixture_and_delete,
5+
create_first_free_fixture_and_delete,
6+
)
7+
8+
# The platform allows one active footer per language and per account and only accepts the
9+
# cultures it ships with. The create call is the claim on a language: a run tries these
10+
# supported codes in order and moves on when the platform reports the language as taken, so
11+
# concurrent runs sharing the operations account cannot both win the same code.
12+
FOOTER_LANGUAGE_CODES = ("hu-HU", "nb-NO", "pt-PT", "ja-JP", "nl-NL", "it-IT", "fr-FR", "cs-CZ")
13+
14+
15+
def _is_language_taken(error):
16+
return "already exists" in str(error)
17+
18+
19+
@pytest.fixture
20+
def footers_service(mpt_ops):
21+
return mpt_ops.notifications.footers
22+
23+
24+
@pytest.fixture
25+
def async_footers_service(async_mpt_ops):
26+
return async_mpt_ops.notifications.footers
27+
28+
29+
@pytest.fixture
30+
def footer_data(short_uuid):
31+
return {"content": f"e2e footer - please delete {short_uuid}"}
32+
33+
34+
@pytest.fixture
35+
def footer_candidates(footer_data):
36+
return [{**footer_data, "languageCode": code} for code in FOOTER_LANGUAGE_CODES]
37+
38+
39+
@pytest.fixture
40+
def created_footer(footers_service, footer_candidates):
41+
with create_first_free_fixture_and_delete(
42+
footers_service, footer_candidates, _is_language_taken
43+
) as footer:
44+
yield footer
45+
46+
47+
@pytest.fixture
48+
async def async_created_footer(async_footers_service, footer_candidates):
49+
async with async_create_first_free_fixture_and_delete(
50+
async_footers_service, footer_candidates, _is_language_taken
51+
) as footer:
52+
yield footer
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from tests.e2e.helper import (
5+
assert_async_service_filter_with_iterate,
6+
assert_async_update_resource,
7+
)
8+
9+
pytestmark = [pytest.mark.flaky]
10+
11+
12+
def test_create_footer(async_created_footer, footer_data):
13+
result = async_created_footer.content
14+
15+
assert result == footer_data["content"]
16+
17+
18+
async def test_get_footer(async_footers_service, async_created_footer):
19+
result = await async_footers_service.get(async_created_footer.id)
20+
21+
assert result.id == async_created_footer.id
22+
23+
24+
async def test_get_footer_not_found(async_footers_service):
25+
bogus_id = "FLV-0000-0000"
26+
27+
with pytest.raises(MPTAPIError, match=r"404 Not Found"):
28+
await async_footers_service.get(bogus_id)
29+
30+
31+
async def test_update_footer(async_footers_service, async_created_footer, short_uuid):
32+
new_content = f"e2e footer updated {short_uuid}"
33+
34+
await assert_async_update_resource( # act
35+
async_footers_service, async_created_footer.id, "content", new_content
36+
)
37+
38+
39+
async def test_delete_footer(async_footers_service, async_created_footer):
40+
await async_footers_service.delete(async_created_footer.id)
41+
42+
result = await async_footers_service.get(async_created_footer.id)
43+
44+
assert result.status == "Deleted"
45+
46+
47+
async def test_delete_footer_not_found(async_footers_service):
48+
bogus_id = "FLV-0000-0000"
49+
50+
with pytest.raises(MPTAPIError):
51+
await async_footers_service.delete(bogus_id)
52+
53+
54+
async def test_filter_footers(async_footers_service, async_created_footer):
55+
await assert_async_service_filter_with_iterate( # act
56+
async_footers_service, async_created_footer.id, None
57+
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
3+
from mpt_api_client.exceptions import MPTAPIError
4+
from tests.e2e.helper import assert_service_filter_with_iterate, assert_update_resource
5+
6+
pytestmark = [pytest.mark.flaky]
7+
8+
9+
def test_create_footer(created_footer, footer_data):
10+
result = created_footer.content
11+
12+
assert result == footer_data["content"]
13+
14+
15+
def test_get_footer(footers_service, created_footer):
16+
result = footers_service.get(created_footer.id)
17+
18+
assert result.id == created_footer.id
19+
20+
21+
def test_get_footer_not_found(footers_service):
22+
bogus_id = "FLV-0000-0000"
23+
24+
with pytest.raises(MPTAPIError, match=r"404 Not Found"):
25+
footers_service.get(bogus_id)
26+
27+
28+
def test_update_footer(footers_service, created_footer, short_uuid):
29+
new_content = f"e2e footer updated {short_uuid}"
30+
31+
assert_update_resource(footers_service, created_footer.id, "content", new_content) # act
32+
33+
34+
def test_delete_footer(footers_service, created_footer):
35+
footers_service.delete(created_footer.id)
36+
37+
result = footers_service.get(created_footer.id)
38+
39+
assert result.status == "Deleted"
40+
41+
42+
def test_delete_footer_not_found(footers_service):
43+
bogus_id = "FLV-0000-0000"
44+
45+
with pytest.raises(MPTAPIError):
46+
footers_service.delete(bogus_id)
47+
48+
49+
def test_filter_footers(footers_service, created_footer):
50+
assert_service_filter_with_iterate(footers_service, created_footer.id, None) # act

0 commit comments

Comments
 (0)