diracx-testing 0.0.1a23__py3-none-any.whl → 0.0.1a25__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,751 +1,47 @@
1
1
  from __future__ import annotations
2
2
 
3
- # TODO: this needs a lot of documentation, in particular what will matter for users
4
- # are the enabled_dependencies markers
5
- import asyncio
6
- import contextlib
7
- import os
8
- import re
9
- import ssl
10
- import subprocess
11
- import tomllib
12
- from collections import defaultdict
13
- from datetime import datetime, timedelta, timezone
14
- from functools import partial
15
- from html.parser import HTMLParser
16
- from importlib.metadata import PackageNotFoundError, distribution, entry_points
17
- from pathlib import Path
18
- from typing import TYPE_CHECKING
19
- from urllib.parse import parse_qs, urljoin, urlparse
20
- from uuid import uuid4
21
-
22
- import pytest
23
- import requests
24
-
25
- if TYPE_CHECKING:
26
- from diracx.core.settings import DevelopmentSettings
27
- from diracx.routers.jobs.sandboxes import SandboxStoreSettings
28
- from diracx.routers.utils.users import AuthorizedUserInfo, AuthSettings
29
-
30
-
31
- # to get a string like this run:
32
- # openssl rand -hex 32
33
- ALGORITHM = "HS256"
34
- ISSUER = "http://lhcbdirac.cern.ch/"
35
- AUDIENCE = "dirac"
36
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
37
-
38
-
39
- def pytest_addoption(parser):
40
- parser.addoption(
41
- "--regenerate-client",
42
- action="store_true",
43
- default=False,
44
- help="Regenerate the AutoREST client",
45
- )
46
- parser.addoption(
47
- "--demo-dir",
48
- type=Path,
49
- default=None,
50
- help="Path to a diracx-charts directory with the demo running",
51
- )
52
-
53
-
54
- def pytest_collection_modifyitems(config, items):
55
- """Disable the test_regenerate_client if not explicitly asked for."""
56
- if config.getoption("--regenerate-client"):
57
- # --regenerate-client given in cli: allow client re-generation
58
- return
59
- skip_regen = pytest.mark.skip(reason="need --regenerate-client option to run")
60
- for item in items:
61
- if item.name == "test_regenerate_client":
62
- item.add_marker(skip_regen)
63
-
64
-
65
- @pytest.fixture(scope="session")
66
- def private_key_pem() -> str:
67
- from cryptography.hazmat.primitives import serialization
68
- from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
69
-
70
- private_key = Ed25519PrivateKey.generate()
71
- return private_key.private_bytes(
72
- encoding=serialization.Encoding.PEM,
73
- format=serialization.PrivateFormat.PKCS8,
74
- encryption_algorithm=serialization.NoEncryption(),
75
- ).decode()
76
-
77
-
78
- @pytest.fixture(scope="session")
79
- def fernet_key() -> str:
80
- from cryptography.fernet import Fernet
81
-
82
- return Fernet.generate_key().decode()
83
-
84
-
85
- @pytest.fixture(scope="session")
86
- def test_dev_settings() -> DevelopmentSettings:
87
- from diracx.core.settings import DevelopmentSettings
88
-
89
- yield DevelopmentSettings()
90
-
91
-
92
- @pytest.fixture(scope="session")
93
- def test_auth_settings(private_key_pem, fernet_key) -> AuthSettings:
94
- from diracx.routers.utils.users import AuthSettings
95
-
96
- yield AuthSettings(
97
- token_algorithm="EdDSA",
98
- token_key=private_key_pem,
99
- state_key=fernet_key,
100
- allowed_redirects=[
101
- "http://diracx.test.invalid:8000/api/docs/oauth2-redirect",
102
- ],
103
- )
104
-
105
-
106
- @pytest.fixture(scope="session")
107
- def aio_moto(worker_id):
108
- """Start the moto server in a separate thread and return the base URL.
109
-
110
- The mocking provided by moto doesn't play nicely with aiobotocore so we use
111
- the server directly. See https://github.com/aio-libs/aiobotocore/issues/755
112
- """
113
- from moto.server import ThreadedMotoServer
114
-
115
- port = 27132
116
- if worker_id != "master":
117
- port += int(worker_id.replace("gw", "")) + 1
118
- server = ThreadedMotoServer(port=port)
119
- server.start()
120
- yield {
121
- "endpoint_url": f"http://localhost:{port}",
122
- "aws_access_key_id": "testing",
123
- "aws_secret_access_key": "testing",
124
- }
125
- server.stop()
126
-
127
-
128
- @pytest.fixture(scope="session")
129
- def test_sandbox_settings(aio_moto) -> SandboxStoreSettings:
130
- from diracx.routers.jobs.sandboxes import SandboxStoreSettings
131
-
132
- yield SandboxStoreSettings(
133
- bucket_name="sandboxes",
134
- s3_client_kwargs=aio_moto,
135
- auto_create_bucket=True,
136
- )
137
-
138
-
139
- class UnavailableDependency:
140
- def __init__(self, key):
141
- self.key = key
142
-
143
- def __call__(self):
144
- raise NotImplementedError(
145
- f"{self.key} has not been made available to this test!"
146
- )
147
-
148
-
149
- class ClientFactory:
150
-
151
- def __init__(
152
- self,
153
- tmp_path_factory,
154
- with_config_repo,
155
- test_auth_settings,
156
- test_sandbox_settings,
157
- test_dev_settings,
158
- ):
159
- from diracx.core.config import ConfigSource
160
- from diracx.core.extensions import select_from_extension
161
- from diracx.core.settings import ServiceSettingsBase
162
- from diracx.db.os.utils import BaseOSDB
163
- from diracx.db.sql.utils import BaseSQLDB
164
- from diracx.routers import create_app_inner
165
- from diracx.routers.access_policies import BaseAccessPolicy
166
-
167
- from .mock_osdb import fake_available_osdb_implementations
168
-
169
- class AlwaysAllowAccessPolicy(BaseAccessPolicy):
170
- """Dummy access policy."""
171
-
172
- async def policy(
173
- policy_name: str, user_info: AuthorizedUserInfo, /, **kwargs
174
- ):
175
- pass
176
-
177
- def enrich_tokens(access_payload: dict, refresh_payload: dict):
178
-
179
- return {"PolicySpecific": "OpenAccessForTest"}, {}
180
-
181
- enabled_systems = {
182
- e.name for e in select_from_extension(group="diracx.services")
183
- }
184
- database_urls = {
185
- e.name: "sqlite+aiosqlite:///:memory:"
186
- for e in select_from_extension(group="diracx.db.sql")
187
- }
188
- # TODO: Monkeypatch this in a less stupid way
189
- # TODO: Only use this if opensearch isn't available
190
- os_database_conn_kwargs = {
191
- e.name: {"sqlalchemy_dsn": "sqlite+aiosqlite:///:memory:"}
192
- for e in select_from_extension(group="diracx.db.os")
193
- }
194
- BaseOSDB.available_implementations = partial(
195
- fake_available_osdb_implementations,
196
- real_available_implementations=BaseOSDB.available_implementations,
197
- )
198
-
199
- self._cache_dir = tmp_path_factory.mktemp("empty-dbs")
200
-
201
- self.test_auth_settings = test_auth_settings
202
- self.test_dev_settings = test_dev_settings
203
-
204
- all_access_policies = {
205
- e.name: [AlwaysAllowAccessPolicy]
206
- + BaseAccessPolicy.available_implementations(e.name)
207
- for e in select_from_extension(group="diracx.access_policies")
208
- }
209
-
210
- self.app = create_app_inner(
211
- enabled_systems=enabled_systems,
212
- all_service_settings=[
213
- test_auth_settings,
214
- test_sandbox_settings,
215
- test_dev_settings,
216
- ],
217
- database_urls=database_urls,
218
- os_database_conn_kwargs=os_database_conn_kwargs,
219
- config_source=ConfigSource.create_from_url(
220
- backend_url=f"git+file://{with_config_repo}"
221
- ),
222
- all_access_policies=all_access_policies,
223
- )
224
-
225
- self.all_dependency_overrides = self.app.dependency_overrides.copy()
226
- self.app.dependency_overrides = {}
227
- for obj in self.all_dependency_overrides:
228
- assert issubclass(
229
- obj.__self__,
230
- (
231
- ServiceSettingsBase,
232
- BaseSQLDB,
233
- BaseOSDB,
234
- ConfigSource,
235
- BaseAccessPolicy,
236
- ),
237
- ), obj
238
-
239
- self.all_lifetime_functions = self.app.lifetime_functions[:]
240
- self.app.lifetime_functions = []
241
- for obj in self.all_lifetime_functions:
242
- assert isinstance(
243
- obj.__self__, (ServiceSettingsBase, BaseSQLDB, BaseOSDB, ConfigSource)
244
- ), obj
245
-
246
- @contextlib.contextmanager
247
- def configure(self, enabled_dependencies):
248
-
249
- assert (
250
- self.app.dependency_overrides == {} and self.app.lifetime_functions == []
251
- ), "configure cannot be nested"
252
- for k, v in self.all_dependency_overrides.items():
253
-
254
- class_name = k.__self__.__name__
255
-
256
- if class_name in enabled_dependencies:
257
- self.app.dependency_overrides[k] = v
258
- else:
259
- self.app.dependency_overrides[k] = UnavailableDependency(class_name)
260
-
261
- for obj in self.all_lifetime_functions:
262
- # TODO: We should use the name of the entry point instead of the class name
263
- if obj.__self__.__class__.__name__ in enabled_dependencies:
264
- self.app.lifetime_functions.append(obj)
265
-
266
- # Add create_db_schemas to the end of the lifetime_functions so that the
267
- # other lifetime_functions (i.e. those which run db.engine_context) have
268
- # already been ran
269
- self.app.lifetime_functions.append(self.create_db_schemas)
270
-
271
- try:
272
- yield
273
- finally:
274
- self.app.dependency_overrides = {}
275
- self.app.lifetime_functions = []
276
-
277
- @contextlib.asynccontextmanager
278
- async def create_db_schemas(self):
279
- """Create DB schema's based on the DBs available in app.dependency_overrides."""
280
- import aiosqlite
281
- import sqlalchemy
282
- from sqlalchemy.util.concurrency import greenlet_spawn
283
-
284
- from diracx.db.sql.utils import BaseSQLDB
285
-
286
- for k, v in self.app.dependency_overrides.items():
287
- # Ignore dependency overrides which aren't BaseSQLDB.transaction
288
- if (
289
- isinstance(v, UnavailableDependency)
290
- or k.__func__ != BaseSQLDB.transaction.__func__
291
- ):
292
- continue
293
- # The first argument of the overridden BaseSQLDB.transaction is the DB object
294
- db = v.args[0]
295
- assert isinstance(db, BaseSQLDB), (k, db)
296
-
297
- # set PRAGMA foreign_keys=ON if sqlite
298
- if db.engine.url.drivername.startswith("sqlite"):
299
-
300
- def set_sqlite_pragma(dbapi_connection, connection_record):
301
- cursor = dbapi_connection.cursor()
302
- cursor.execute("PRAGMA foreign_keys=ON")
303
- cursor.close()
304
-
305
- sqlalchemy.event.listen(
306
- db.engine.sync_engine, "connect", set_sqlite_pragma
307
- )
308
-
309
- # We maintain a cache of the populated DBs in empty_db_dir so that
310
- # we don't have to recreate them for every test. This speeds up the
311
- # tests by a considerable amount.
312
- ref_db = self._cache_dir / f"{k.__self__.__name__}.db"
313
- if ref_db.exists():
314
- async with aiosqlite.connect(ref_db) as ref_conn:
315
- conn = await db.engine.raw_connection()
316
- await ref_conn.backup(conn.driver_connection)
317
- await greenlet_spawn(conn.close)
318
- else:
319
- async with db.engine.begin() as conn:
320
- await conn.run_sync(db.metadata.create_all)
321
-
322
- async with aiosqlite.connect(ref_db) as ref_conn:
323
- conn = await db.engine.raw_connection()
324
- await conn.driver_connection.backup(ref_conn)
325
- await greenlet_spawn(conn.close)
326
-
327
- yield
328
-
329
- @contextlib.contextmanager
330
- def unauthenticated(self):
331
- from fastapi.testclient import TestClient
332
-
333
- with TestClient(self.app) as client:
334
- yield client
335
-
336
- @contextlib.contextmanager
337
- def normal_user(self):
338
- from diracx.core.properties import NORMAL_USER
339
- from diracx.routers.auth.token import create_token
340
-
341
- with self.unauthenticated() as client:
342
- payload = {
343
- "sub": "testingVO:yellow-sub",
344
- "exp": datetime.now(tz=timezone.utc)
345
- + timedelta(self.test_auth_settings.access_token_expire_minutes),
346
- "iss": ISSUER,
347
- "dirac_properties": [NORMAL_USER],
348
- "jti": str(uuid4()),
349
- "preferred_username": "preferred_username",
350
- "dirac_group": "test_group",
351
- "vo": "lhcb",
352
- }
353
- token = create_token(payload, self.test_auth_settings)
354
-
355
- client.headers["Authorization"] = f"Bearer {token}"
356
- client.dirac_token_payload = payload
357
- yield client
358
-
359
- @contextlib.contextmanager
360
- def admin_user(self):
361
- from diracx.core.properties import JOB_ADMINISTRATOR
362
- from diracx.routers.auth.token import create_token
363
-
364
- with self.unauthenticated() as client:
365
- payload = {
366
- "sub": "testingVO:yellow-sub",
367
- "iss": ISSUER,
368
- "dirac_properties": [JOB_ADMINISTRATOR],
369
- "jti": str(uuid4()),
370
- "preferred_username": "preferred_username",
371
- "dirac_group": "test_group",
372
- "vo": "lhcb",
373
- }
374
- token = create_token(payload, self.test_auth_settings)
375
- client.headers["Authorization"] = f"Bearer {token}"
376
- client.dirac_token_payload = payload
377
- yield client
378
-
379
-
380
- @pytest.fixture(scope="session")
381
- def session_client_factory(
3
+ from .entrypoints import verify_entry_points
4
+ from .utils import (
5
+ ClientFactory,
6
+ aio_moto,
7
+ cli_env,
8
+ client_factory,
9
+ demo_dir,
10
+ demo_kubectl_env,
11
+ demo_urls,
12
+ do_device_flow_with_dex,
13
+ fernet_key,
14
+ private_key_pem,
15
+ pytest_addoption,
16
+ pytest_collection_modifyitems,
17
+ session_client_factory,
382
18
  test_auth_settings,
19
+ test_dev_settings,
20
+ test_login,
383
21
  test_sandbox_settings,
22
+ with_cli_login,
384
23
  with_config_repo,
385
- tmp_path_factory,
386
- test_dev_settings,
387
- ):
388
- """TODO.
389
- ----
390
-
391
- """
392
- yield ClientFactory(
393
- tmp_path_factory,
394
- with_config_repo,
395
- test_auth_settings,
396
- test_sandbox_settings,
397
- test_dev_settings,
398
- )
399
-
400
-
401
- @pytest.fixture
402
- def client_factory(session_client_factory, request):
403
- marker = request.node.get_closest_marker("enabled_dependencies")
404
- if marker is None:
405
- raise RuntimeError("This test requires the enabled_dependencies marker")
406
- (enabled_dependencies,) = marker.args
407
- with session_client_factory.configure(enabled_dependencies=enabled_dependencies):
408
- yield session_client_factory
409
-
410
-
411
- @pytest.fixture(scope="session")
412
- def with_config_repo(tmp_path_factory):
413
- from git import Repo
414
-
415
- from diracx.core.config import Config
416
-
417
- tmp_path = tmp_path_factory.mktemp("cs-repo")
418
-
419
- repo = Repo.init(tmp_path, initial_branch="master")
420
- cs_file = tmp_path / "default.yml"
421
- example_cs = Config.model_validate(
422
- {
423
- "DIRAC": {},
424
- "Registry": {
425
- "lhcb": {
426
- "DefaultGroup": "lhcb_user",
427
- "DefaultProxyLifeTime": 432000,
428
- "DefaultStorageQuota": 2000,
429
- "IdP": {
430
- "URL": "https://idp-server.invalid",
431
- "ClientID": "test-idp",
432
- },
433
- "Users": {
434
- "b824d4dc-1f9d-4ee8-8df5-c0ae55d46041": {
435
- "PreferedUsername": "chaen",
436
- "Email": None,
437
- },
438
- "c935e5ed-2g0e-5ff9-9eg6-d1bf66e57152": {
439
- "PreferedUsername": "albdr",
440
- "Email": None,
441
- },
442
- },
443
- "Groups": {
444
- "lhcb_user": {
445
- "Properties": ["NormalUser", "PrivateLimitedDelegation"],
446
- "Users": [
447
- "b824d4dc-1f9d-4ee8-8df5-c0ae55d46041",
448
- "c935e5ed-2g0e-5ff9-9eg6-d1bf66e57152",
449
- ],
450
- },
451
- "lhcb_prmgr": {
452
- "Properties": ["NormalUser", "ProductionManagement"],
453
- "Users": ["b824d4dc-1f9d-4ee8-8df5-c0ae55d46041"],
454
- },
455
- "lhcb_tokenmgr": {
456
- "Properties": ["NormalUser", "ProxyManagement"],
457
- "Users": ["c935e5ed-2g0e-5ff9-9eg6-d1bf66e57152"],
458
- },
459
- },
460
- }
461
- },
462
- "Operations": {"Defaults": {}},
463
- "Systems": {
464
- "WorkloadManagement": {
465
- "Production": {
466
- "Databases": {
467
- "JobDB": {
468
- "DBName": "xyz",
469
- "Host": "xyz",
470
- "Port": 9999,
471
- "MaxRescheduling": 3,
472
- },
473
- "JobLoggingDB": {
474
- "DBName": "xyz",
475
- "Host": "xyz",
476
- "Port": 9999,
477
- },
478
- "PilotAgentsDB": {
479
- "DBName": "xyz",
480
- "Host": "xyz",
481
- "Port": 9999,
482
- },
483
- "SandboxMetadataDB": {
484
- "DBName": "xyz",
485
- "Host": "xyz",
486
- "Port": 9999,
487
- },
488
- "TaskQueueDB": {
489
- "DBName": "xyz",
490
- "Host": "xyz",
491
- "Port": 9999,
492
- },
493
- "ElasticJobParametersDB": {
494
- "DBName": "xyz",
495
- "Host": "xyz",
496
- "Port": 9999,
497
- },
498
- "VirtualMachineDB": {
499
- "DBName": "xyz",
500
- "Host": "xyz",
501
- "Port": 9999,
502
- },
503
- },
504
- },
505
- },
506
- },
507
- }
508
- )
509
- cs_file.write_text(example_cs.model_dump_json())
510
- repo.index.add([cs_file]) # add it to the index
511
- repo.index.commit("Added a new file")
512
- yield tmp_path
513
-
514
-
515
- @pytest.fixture(scope="session")
516
- def demo_dir(request) -> Path:
517
- demo_dir = request.config.getoption("--demo-dir")
518
- if demo_dir is None:
519
- pytest.skip("Requires a running instance of the DiracX demo")
520
- demo_dir = (demo_dir / ".demo").resolve()
521
- yield demo_dir
522
-
523
-
524
- @pytest.fixture(scope="session")
525
- def demo_urls(demo_dir):
526
- import yaml
527
-
528
- helm_values = yaml.safe_load((demo_dir / "values.yaml").read_text())
529
- yield helm_values["developer"]["urls"]
530
-
531
-
532
- @pytest.fixture(scope="session")
533
- def demo_kubectl_env(demo_dir):
534
- """Get the dictionary of environment variables for kubectl to control the demo."""
535
- kube_conf = demo_dir / "kube.conf"
536
- if not kube_conf.exists():
537
- raise RuntimeError(f"Could not find {kube_conf}, is the demo running?")
538
-
539
- env = {
540
- **os.environ,
541
- "KUBECONFIG": str(kube_conf),
542
- "PATH": f"{demo_dir}:{os.environ['PATH']}",
543
- }
544
-
545
- # Check that we can run kubectl
546
- pods_result = subprocess.check_output(
547
- ["kubectl", "get", "pods"], env=env, text=True
548
- )
549
- assert "diracx" in pods_result
550
-
551
- yield env
552
-
553
-
554
- @pytest.fixture
555
- def cli_env(monkeypatch, tmp_path, demo_urls, demo_dir):
556
- """Set up the environment for the CLI."""
557
- import httpx
558
-
559
- from diracx.core.preferences import get_diracx_preferences
560
-
561
- diracx_url = demo_urls["diracx"]
562
- ca_path = demo_dir / "demo-ca.pem"
563
- if not ca_path.exists():
564
- raise RuntimeError(f"Could not find {ca_path}, is the demo running?")
565
-
566
- # Ensure the demo is working
567
-
568
- r = httpx.get(
569
- f"{diracx_url}/api/openapi.json",
570
- verify=ssl.create_default_context(cafile=ca_path),
571
- )
572
- r.raise_for_status()
573
- assert r.json()["info"]["title"] == "Dirac"
574
-
575
- env = {
576
- "DIRACX_URL": diracx_url,
577
- "DIRACX_CA_PATH": str(ca_path),
578
- "HOME": str(tmp_path),
579
- }
580
- for key, value in env.items():
581
- monkeypatch.setenv(key, value)
582
- yield env
583
-
584
- # The DiracX preferences are cached however when testing this cache is invalid
585
- get_diracx_preferences.cache_clear()
586
-
587
-
588
- @pytest.fixture
589
- async def with_cli_login(monkeypatch, capfd, cli_env, tmp_path):
590
- try:
591
- credentials = await test_login(monkeypatch, capfd, cli_env)
592
- except Exception as e:
593
- pytest.skip(f"Login failed, fix test_login to re-enable this test: {e!r}")
594
-
595
- credentials_path = tmp_path / "credentials.json"
596
- credentials_path.write_text(credentials)
597
- monkeypatch.setenv("DIRACX_CREDENTIALS_PATH", str(credentials_path))
598
- yield
599
-
600
-
601
- async def test_login(monkeypatch, capfd, cli_env):
602
- from diracx import cli
603
-
604
- poll_attempts = 0
605
-
606
- def fake_sleep(*args, **kwargs):
607
- nonlocal poll_attempts
608
-
609
- # Keep track of the number of times this is called
610
- poll_attempts += 1
611
-
612
- # After polling 5 times, do the actual login
613
- if poll_attempts == 5:
614
- # The login URL should have been printed to stdout
615
- captured = capfd.readouterr()
616
- match = re.search(rf"{cli_env['DIRACX_URL']}[^\n]+", captured.out)
617
- assert match, captured
618
-
619
- do_device_flow_with_dex(match.group(), cli_env["DIRACX_CA_PATH"])
620
-
621
- # Ensure we don't poll forever
622
- assert poll_attempts <= 100
623
-
624
- # Reduce the sleep duration to zero to speed up the test
625
- return unpatched_sleep(0)
626
-
627
- # We monkeypatch asyncio.sleep to provide a hook to run the actions that
628
- # would normally be done by a user. This includes capturing the login URL
629
- # and doing the actual device flow with dex.
630
- unpatched_sleep = asyncio.sleep
631
-
632
- expected_credentials_path = Path(
633
- cli_env["HOME"], ".cache", "diracx", "credentials.json"
634
- )
635
- # Ensure the credentials file does not exist before logging in
636
- assert not expected_credentials_path.exists()
637
-
638
- # Run the login command
639
- with monkeypatch.context() as m:
640
- m.setattr("asyncio.sleep", fake_sleep)
641
- await cli.login(vo="diracAdmin", group=None, property=None)
642
- captured = capfd.readouterr()
643
- assert "Login successful!" in captured.out
644
- assert captured.err == ""
645
-
646
- # Ensure the credentials file exists after logging in
647
- assert expected_credentials_path.exists()
648
-
649
- # Return the credentials so this test can also be used by the
650
- # "with_cli_login" fixture
651
- return expected_credentials_path.read_text()
652
-
653
-
654
- def do_device_flow_with_dex(url: str, ca_path: str) -> None:
655
- """Do the device flow with dex."""
656
-
657
- class DexLoginFormParser(HTMLParser):
658
- def handle_starttag(self, tag, attrs):
659
- nonlocal action_url
660
- if "form" in str(tag):
661
- assert action_url is None
662
- action_url = urljoin(login_page_url, dict(attrs)["action"])
663
-
664
- # Get the login page
665
- r = requests.get(url, verify=ca_path)
666
- r.raise_for_status()
667
- login_page_url = r.url # This is not the same as URL as we redirect to dex
668
- login_page_body = r.text
669
-
670
- # Search the page for the login form so we know where to post the credentials
671
- action_url = None
672
- DexLoginFormParser().feed(login_page_body)
673
- assert action_url is not None, login_page_body
674
-
675
- # Do the actual login
676
- r = requests.post(
677
- action_url,
678
- data={"login": "admin@example.com", "password": "password"},
679
- verify=ca_path,
680
- )
681
- r.raise_for_status()
682
- approval_url = r.url # This is not the same as URL as we redirect to dex
683
- # Do the actual approval
684
- r = requests.post(
685
- approval_url,
686
- {"approval": "approve", "req": parse_qs(urlparse(r.url).query)["req"][0]},
687
- verify=ca_path,
688
- )
689
-
690
- # This should have redirected to the DiracX page that shows the login is complete
691
- assert "Please close the window" in r.text
692
-
693
-
694
- def get_installed_entry_points():
695
- """Retrieve the installed entry points from the environment."""
696
- entry_pts = entry_points()
697
- diracx_eps = defaultdict(dict)
698
- for group in entry_pts.groups:
699
- if "diracx" in group:
700
- for ep in entry_pts.select(group=group):
701
- diracx_eps[group][ep.name] = ep.value
702
- return dict(diracx_eps)
703
-
704
-
705
- def get_entry_points_from_toml(toml_file):
706
- """Parse entry points from pyproject.toml."""
707
- with open(toml_file, "rb") as f:
708
- pyproject = tomllib.load(f)
709
- package_name = pyproject["project"]["name"]
710
- return package_name, pyproject.get("project", {}).get("entry-points", {})
711
-
712
-
713
- def get_current_entry_points(repo_base) -> bool:
714
- """Create current entry points dict for comparison."""
715
- current_eps = {}
716
- for toml_file in repo_base.glob("diracx-*/pyproject.toml"):
717
- package_name, entry_pts = get_entry_points_from_toml(f"{toml_file}")
718
- # Ignore packages that are not installed
719
- try:
720
- distribution(package_name)
721
- except PackageNotFoundError:
722
- continue
723
- # Merge the entry points
724
- for key, value in entry_pts.items():
725
- current_eps[key] = current_eps.get(key, {}) | value
726
- return current_eps
727
-
728
-
729
- @pytest.fixture(scope="session", autouse=True)
730
- def verify_entry_points(request, pytestconfig):
731
- try:
732
- ini_toml_name = tomllib.loads(pytestconfig.inipath.read_text())["project"][
733
- "name"
734
- ]
735
- except tomllib.TOMLDecodeError:
736
- return
737
- if ini_toml_name == "diracx":
738
- repo_base = pytestconfig.inipath.parent
739
- elif ini_toml_name.startswith("diracx-"):
740
- repo_base = pytestconfig.inipath.parent.parent
741
- else:
742
- return
743
-
744
- installed_eps = get_installed_entry_points()
745
- current_eps = get_current_entry_points(repo_base)
746
-
747
- if installed_eps != current_eps:
748
- pytest.fail(
749
- "Project and installed entry-points are not consistent. "
750
- "You should run `pip install -r requirements-dev.txt`",
751
- )
24
+ )
25
+
26
+ __all__ = (
27
+ "verify_entry_points",
28
+ "ClientFactory",
29
+ "do_device_flow_with_dex",
30
+ "test_login",
31
+ "pytest_addoption",
32
+ "pytest_collection_modifyitems",
33
+ "private_key_pem",
34
+ "fernet_key",
35
+ "test_dev_settings",
36
+ "test_auth_settings",
37
+ "aio_moto",
38
+ "test_sandbox_settings",
39
+ "session_client_factory",
40
+ "client_factory",
41
+ "with_config_repo",
42
+ "demo_dir",
43
+ "demo_urls",
44
+ "demo_kubectl_env",
45
+ "cli_env",
46
+ "with_cli_login",
47
+ )