Skip to content

Commit e7ad23a

Browse files
authored
Merge pull request #40 from maykinmedia/slug-migrations
Handle duplicate service slugs
2 parents 5d331a6 + b420445 commit e7ad23a

File tree

3 files changed

+185
-113
lines changed

3 files changed

+185
-113
lines changed

setup.cfg

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ install_requires =
4545
isodate
4646
notifications-api-common>=0.3.0
4747
zgw-consumers>=0.35.1
48-
zgw-consumers-oas
4948
oyaml
5049
PyJWT>=2.0.0
5150
requests
@@ -61,6 +60,7 @@ tests_require =
6160
black
6261
requests-mock
6362
freezegun
63+
zgw-consumers-oas
6464

6565
[options.packages.find]
6666
include =
@@ -80,6 +80,7 @@ tests =
8080
black
8181
requests-mock
8282
freezegun
83+
zgw-consumers-oas
8384
pep8 = flake8
8485
coverage = pytest-cov
8586
docs =

tests/test_migrations.py

+145-87
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from typing import Callable, Generator
2+
13
from django.apps import apps as global_apps
4+
from django.apps.registry import Apps
25
from django.db import connection
36
from django.db.migrations.executor import MigrationExecutor
47
from django.test import override_settings
@@ -13,93 +16,88 @@ class BaseMigrationTest:
1316
migrate_to = None # The migration we want to test
1417
setting_overrides = {}
1518

16-
@pytest.fixture(autouse=True)
17-
def setup_migration(self, db):
19+
@pytest.fixture
20+
def setup_migration(self, db) -> Generator:
1821
"""
1922
Setup the migration test by reversing to `migrate_from` state,
2023
then applying the `migrate_to` state.
2124
"""
22-
assert self.app is not None, "You must define the `app` attribute"
23-
assert self.migrate_from is not None, "You must define `migrate_from`"
24-
assert self.migrate_to is not None, "You must define `migrate_to`"
2525

26-
# Step 1: Set up the MigrationExecutor
27-
executor = MigrationExecutor(connection)
26+
def _migrate(callback: Callable) -> None:
27+
assert self.app is not None, "You must define the `app` attribute"
28+
assert self.migrate_from is not None, "You must define `migrate_from`"
29+
assert self.migrate_to is not None, "You must define `migrate_to`"
30+
31+
# Step 1: Set up the MigrationExecutor
32+
executor = MigrationExecutor(connection)
2833

29-
# Step 2: Reverse to the starting migration state
30-
migrate_from = [(self.app, self.migrate_from)]
31-
migrate_to = [(self.app, self.migrate_to)]
32-
old_migrate_state = executor.migrate(migrate_from)
33-
old_apps = old_migrate_state.apps
34+
# Step 2: Reverse to the starting migration state
35+
migrate_from = [(self.app, self.migrate_from)]
36+
migrate_to = [(self.app, self.migrate_to)]
37+
old_migrate_state = executor.migrate(migrate_from)
38+
old_apps = old_migrate_state.apps
3439

35-
# Step 3: Run the `setup_before_migration` hook with old state models
36-
self.setup_before_migration(old_apps)
40+
# Step 3: Call `callback` old state models
41+
callback(old_apps)
3742

38-
# Step 4: Apply the migration we want to test with any settings overrides
39-
with override_settings(**self.setting_overrides):
43+
# Step 4: Apply the migration we want to test with any settings overrides
4044
executor = MigrationExecutor(connection)
4145
executor.loader.build_graph() # reload the graph in case of dependency changes
4246
executor.migrate(migrate_to)
4347

44-
# Set `self.apps` to the post-migration state
45-
self.apps = executor.loader.project_state(migrate_to).apps
46-
47-
def setup_before_migration(self, apps):
48-
"""
49-
Hook to set up the state before the migration. Subclasses can
50-
override this to create data or perform other setup actions.
51-
"""
52-
pass
48+
yield _migrate
5349

5450

5551
class TestMigrateAPICredentialToService(BaseMigrationTest):
5652
app = "vng_api_common"
5753
migrate_from = "0005_auto_20190614_1346"
5854
migrate_to = "0006_delete_apicredential"
5955

60-
def setup_before_migration(self, apps):
61-
Service = apps.get_model("zgw_consumers", "Service")
62-
APICredential = apps.get_model("vng_api_common", "APICredential")
63-
64-
APICredential.objects.create(
65-
api_root="https://example.com/zaken/api/v1/",
66-
label="Zaken API",
67-
client_id="user",
68-
secret="secret",
69-
user_id="user",
70-
user_representation="User",
71-
)
56+
def test_migrate_api_credentials_to_services(self, setup_migration: Callable):
57+
def migration_callback(apps: Apps) -> None:
58+
Service = apps.get_model("zgw_consumers", "Service")
59+
APICredential = apps.get_model("vng_api_common", "APICredential")
60+
61+
APICredential.objects.create(
62+
api_root="https://example.com/zaken/api/v1/",
63+
label="Zaken API",
64+
client_id="user",
65+
secret="secret",
66+
user_id="user",
67+
user_representation="User",
68+
)
69+
70+
APICredential.objects.create(
71+
api_root="https://example.com/api/v1/",
72+
label="Selectielijst API",
73+
client_id="user2",
74+
secret="secret2",
75+
user_id="user2",
76+
user_representation="User2",
77+
)
78+
79+
APICredential.objects.create(
80+
api_root="https://example.com/documenten/api/v1/",
81+
label="Documenten API",
82+
client_id="user3",
83+
secret="secret3",
84+
user_id="user3",
85+
user_representation="User3",
86+
)
87+
Service.objects.create(
88+
api_root="https://example.com/documenten/api/v1/",
89+
label="Documenten",
90+
slug="already-exists",
91+
api_type=APITypes.drc,
92+
auth_type=AuthTypes.zgw,
93+
client_id="user4",
94+
secret="secret4",
95+
user_id="user4",
96+
user_representation="User4",
97+
)
98+
99+
setup_migration(migration_callback)
72100

73-
APICredential.objects.create(
74-
api_root="https://example.com/api/v1/",
75-
label="Selectielijst API",
76-
client_id="user2",
77-
secret="secret2",
78-
user_id="user2",
79-
user_representation="User2",
80-
)
81-
82-
APICredential.objects.create(
83-
api_root="https://example.com/documenten/api/v1/",
84-
label="Documenten API",
85-
client_id="user3",
86-
secret="secret3",
87-
user_id="user3",
88-
user_representation="User3",
89-
)
90-
Service.objects.create(
91-
api_root="https://example.com/documenten/api/v1/",
92-
label="Documenten",
93-
slug="already-exists",
94-
api_type=APITypes.drc,
95-
auth_type=AuthTypes.zgw,
96-
client_id="user4",
97-
secret="secret4",
98-
user_id="user4",
99-
user_representation="User4",
100-
)
101-
102-
def test_migrate_api_credentials_to_services(self):
103101
Service = global_apps.get_model("zgw_consumers", "Service")
104102

105103
zaken_service = Service.objects.get(slug="zaken-api")
@@ -136,33 +134,93 @@ def test_migrate_api_credentials_to_services(self):
136134
assert documenten_service.user_id == "user4"
137135
assert documenten_service.user_representation == "User4"
138136

137+
def test_duplicate_service_slug(self, setup_migration):
138+
"""
139+
Test that the migration handles duplicate slugs correctly
140+
"""
141+
142+
def migration_callback(apps):
143+
Service = apps.get_model("zgw_consumers", "Service")
144+
APICredential = apps.get_model("vng_api_common", "APICredential")
145+
146+
APICredential.objects.create(
147+
api_root="https://example.com/documenten/api/v1/",
148+
label="Documenten API",
149+
client_id="user3",
150+
secret="secret3",
151+
user_id="user3",
152+
user_representation="User3",
153+
)
154+
155+
Service.objects.create(
156+
api_root="https://example.com/documenten/api/v2/",
157+
label="Documenten",
158+
slug="documenten-api",
159+
api_type=APITypes.drc,
160+
auth_type=AuthTypes.zgw,
161+
client_id="user4",
162+
secret="secret4",
163+
user_id="user4",
164+
user_representation="User4",
165+
)
166+
167+
setup_migration(migration_callback)
168+
169+
Service = global_apps.get_model("zgw_consumers", "Service")
170+
171+
service_v1 = Service.objects.get(slug="documenten-api-2")
172+
173+
assert service_v1.api_root == "https://example.com/documenten/api/v1/"
174+
assert service_v1.label == "Documenten API"
175+
assert service_v1.api_type == APITypes.drc
176+
assert service_v1.auth_type == AuthTypes.zgw
177+
assert service_v1.client_id == "user3"
178+
assert service_v1.secret == "secret3"
179+
assert service_v1.user_id == "user3"
180+
assert service_v1.user_representation == "User3"
181+
182+
service_v2 = Service.objects.get(slug="documenten-api")
183+
184+
assert service_v2.api_root == "https://example.com/documenten/api/v2/"
185+
assert service_v2.label == "Documenten"
186+
assert service_v2.api_type == APITypes.drc
187+
assert service_v2.auth_type == AuthTypes.zgw
188+
assert service_v2.client_id == "user4"
189+
assert service_v2.secret == "secret4"
190+
assert service_v2.user_id == "user4"
191+
assert service_v2.user_representation == "User4"
192+
139193

140194
class TestMigrateAuthorizationsConfigAPIRootToService(BaseMigrationTest):
141195
app = "authorizations"
142196
migrate_from = "0015_auto_20220318_1608"
143197
migrate_to = "0016_remove_authorizationsconfig_api_root_and_more"
144198

145-
def setup_before_migration(self, apps):
146-
Service = apps.get_model("zgw_consumers", "Service")
147-
AuthorizationsConfig = apps.get_model("authorizations", "AuthorizationsConfig")
148-
149-
config, _ = AuthorizationsConfig.objects.get_or_create()
150-
config.api_root = "https://example.com/autorisaties/api/v1/"
151-
config.save()
152-
153-
self.service = Service.objects.create(
154-
api_root="https://example.com/autorisaties/api/v1/",
155-
label="Autorisaties",
156-
slug="autorisaties",
157-
api_type=APITypes.ac,
158-
auth_type=AuthTypes.zgw,
159-
client_id="user",
160-
secret="secret",
161-
user_id="user",
162-
user_representation="User",
163-
)
199+
def test_migrate_api_root_to_service(self, setup_migration: Callable):
200+
def migration_callback(apps: Apps) -> None:
201+
Service = apps.get_model("zgw_consumers", "Service")
202+
AuthorizationsConfig = apps.get_model(
203+
"authorizations", "AuthorizationsConfig"
204+
)
205+
206+
config, _ = AuthorizationsConfig.objects.get_or_create()
207+
config.api_root = "https://example.com/autorisaties/api/v1/"
208+
config.save()
209+
210+
self.service = Service.objects.create(
211+
api_root="https://example.com/autorisaties/api/v1/",
212+
label="Autorisaties",
213+
slug="autorisaties",
214+
api_type=APITypes.ac,
215+
auth_type=AuthTypes.zgw,
216+
client_id="user",
217+
secret="secret",
218+
user_id="user",
219+
user_representation="User",
220+
)
221+
222+
setup_migration(migration_callback)
164223

165-
def test_migrate_api_root_to_service(self):
166224
AuthorizationsConfig = global_apps.get_model(
167225
"authorizations", "AuthorizationsConfig"
168226
)

vng_api_common/migrations/0006_delete_apicredential.py

+38-25
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
# Generated by Django 5.1.2 on 2024-10-24 13:51
22

33
import logging
4+
from typing import Set
45

5-
from django.db import IntegrityError, migrations
6+
from django.db import migrations, models
67
from django.utils.text import slugify
78

89
from zgw_consumers.constants import APITypes, AuthTypes
910

1011
logger = logging.getLogger(__name__)
1112

1213

13-
def get_api_type(api_root: str) -> APITypes:
14+
def _get_api_type(api_root: str) -> APITypes:
1415
mapping = {
1516
"/autorisaties/api/": APITypes.ac,
1617
"/zaken/api/": APITypes.zrc,
@@ -26,38 +27,50 @@ def get_api_type(api_root: str) -> APITypes:
2627
return APITypes.orc
2728

2829

30+
def _get_service_slug(credential: models.Model, existing_slugs: Set[str]) -> str:
31+
default_slug: str = slugify(credential.label)
32+
33+
if default_slug not in existing_slugs or not existing_slugs:
34+
return default_slug
35+
36+
count = 2
37+
slug = f"{default_slug}-{count}"
38+
39+
while slug in existing_slugs:
40+
count += 1
41+
slug = f"{default_slug}-{count}"
42+
43+
return slug
44+
45+
2946
def migrate_credentials_to_service(apps, _) -> None:
3047
APICredential = apps.get_model("vng_api_common", "APICredential")
3148
Service = apps.get_model("zgw_consumers", "Service")
3249

3350
credentials = APICredential.objects.all()
3451

52+
existings_service_slugs = set(Service.objects.values_list("slug", flat=True))
53+
3554
for credential in credentials:
3655
logger.info(f"Creating Service for {credential.client_id}")
3756

38-
service_slug = slugify(credential.label)
39-
40-
try:
41-
_, created = Service.objects.get_or_create(
42-
api_root=credential.api_root,
43-
defaults=dict(
44-
label=credential.label,
45-
slug=service_slug,
46-
api_type=get_api_type(credential.api_root),
47-
auth_type=AuthTypes.zgw,
48-
client_id=credential.client_id,
49-
secret=credential.secret,
50-
user_id=credential.user_id,
51-
user_representation=credential.user_representation,
52-
),
53-
)
54-
except IntegrityError:
55-
logger.warning(
56-
f"Unable to create Service for {credential.api_root}. Check the"
57-
"`service slug` field on the existing Service's to verify an existing"
58-
" Service already exists."
59-
)
60-
continue
57+
service_slug = _get_service_slug(credential, existings_service_slugs)
58+
59+
_, created = Service.objects.get_or_create(
60+
api_root=credential.api_root,
61+
defaults=dict(
62+
label=credential.label,
63+
slug=service_slug,
64+
api_type=_get_api_type(credential.api_root),
65+
auth_type=AuthTypes.zgw,
66+
client_id=credential.client_id,
67+
secret=credential.secret,
68+
user_id=credential.user_id,
69+
user_representation=credential.user_representation,
70+
),
71+
)
72+
73+
existings_service_slugs.add(service_slug)
6174

6275
if created:
6376
logger.info(f"Created new Service for {credential.api_root}")

0 commit comments

Comments
 (0)