diff --git a/src/diracx/cli/__init__.py b/src/diracx/cli/__init__.py index 47c6842c..4b0d998c 100644 --- a/src/diracx/cli/__init__.py +++ b/src/diracx/cli/__init__.py @@ -1,12 +1,10 @@ -from __future__ import annotations - import asyncio import json import os from datetime import datetime, timedelta -from typing import Optional +from typing import Annotated, Optional -from typer import Option +import typer from diracx.client.aio import DiracClient from diracx.client.models import DeviceFlowErrorResponse @@ -19,11 +17,30 @@ app = AsyncTyper() +async def installation_metadata(): + async with DiracClient() as api: + return await api.well_known.installation_metadata() + + +def vo_callback(vo: str | None) -> str: + metadata = asyncio.run(installation_metadata()) + vos = list(metadata.virtual_organizations) + if not vo: + raise typer.BadParameter( + f"VO must be specified, available options are: {' '.join(vos)}" + ) + if vo not in vos: + raise typer.BadParameter( + f"Unknown VO {vo}, available options are: {' '.join(vos)}" + ) + return vo + + @app.async_command() async def login( - vo: str, + vo: Annotated[Optional[str], typer.Argument(callback=vo_callback)] = None, group: Optional[str] = None, - property: Optional[list[str]] = Option( + property: Optional[list[str]] = typer.Option( None, help="Override the default(s) with one or more properties" ), ): diff --git a/src/diracx/cli/internal/legacy.py b/src/diracx/cli/internal/legacy.py index f7cefea9..c3f091fe 100644 --- a/src/diracx/cli/internal/legacy.py +++ b/src/diracx/cli/internal/legacy.py @@ -13,6 +13,7 @@ from typer import Option from diracx.core.config import Config +from diracx.core.config.schema import Field, SupportInfo from ..utils import AsyncTyper @@ -28,6 +29,7 @@ class VOConfig(BaseModel): DefaultGroup: str IdP: IdPConfig UserSubjects: dict[str, str] + Support: SupportInfo = Field(default_factory=SupportInfo) class ConversionConfig(BaseModel): @@ -105,6 +107,7 @@ def _apply_fixes(raw, conversion_config: Path): "DefaultGroup": vo_meta.DefaultGroup, "Users": {}, "Groups": {}, + "Support": vo_meta.Support, } if "DefaultStorageQuota" in original_registry: raw["Registry"][vo]["DefaultStorageQuota"] = original_registry[ diff --git a/src/diracx/client/aio/operations/_operations.py b/src/diracx/client/aio/operations/_operations.py index a1c87d68..41506aea 100644 --- a/src/diracx/client/aio/operations/_operations.py +++ b/src/diracx/client/aio/operations/_operations.py @@ -43,11 +43,14 @@ build_jobs_get_single_job_status_request, build_jobs_initiate_sandbox_upload_request, build_jobs_kill_bulk_jobs_request, + build_jobs_reschedule_bulk_jobs_request, + build_jobs_reschedule_single_job_request, build_jobs_search_request, build_jobs_set_job_status_bulk_request, build_jobs_set_single_job_status_request, build_jobs_submit_bulk_jobs_request, build_jobs_summary_request, + build_well_known_installation_metadata_request, build_well_known_openid_configuration_request, ) from .._vendor import raise_if_not_implemented @@ -135,6 +138,57 @@ async def openid_configuration(self, **kwargs: Any) -> Any: return deserialized + @distributed_trace_async + async def installation_metadata(self, **kwargs: Any) -> _models.Metadata: + """Installation Metadata. + + Installation Metadata. + + :return: Metadata + :rtype: ~client.models.Metadata + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[_models.Metadata] = kwargs.pop("cls", None) + + request = build_well_known_installation_metadata_request( + headers=_headers, + params=_params, + ) + request.url = self._client.format_url(request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error( + status_code=response.status_code, response=response, error_map=error_map + ) + raise HttpResponseError(response=response) + + deserialized = self._deserialize("Metadata", pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + class AuthOperations: """ @@ -1472,6 +1526,114 @@ async def get_job_status_history_bulk( return deserialized + @distributed_trace_async + async def reschedule_bulk_jobs(self, *, job_ids: List[int], **kwargs: Any) -> Any: + """Reschedule Bulk Jobs. + + Reschedule Bulk Jobs. + + :keyword job_ids: Required. + :paramtype job_ids: list[int] + :return: any + :rtype: any + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[Any] = kwargs.pop("cls", None) + + request = build_jobs_reschedule_bulk_jobs_request( + job_ids=job_ids, + headers=_headers, + params=_params, + ) + request.url = self._client.format_url(request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error( + status_code=response.status_code, response=response, error_map=error_map + ) + raise HttpResponseError(response=response) + + deserialized = self._deserialize("object", pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + + @distributed_trace_async + async def reschedule_single_job(self, job_id: int, **kwargs: Any) -> Any: + """Reschedule Single Job. + + Reschedule Single Job. + + :param job_id: Required. + :type job_id: int + :return: any + :rtype: any + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[Any] = kwargs.pop("cls", None) + + request = build_jobs_reschedule_single_job_request( + job_id=job_id, + headers=_headers, + params=_params, + ) + request.url = self._client.format_url(request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + await self._client._pipeline.run( # pylint: disable=protected-access + request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error( + status_code=response.status_code, response=response, error_map=error_map + ) + raise HttpResponseError(response=response) + + deserialized = self._deserialize("object", pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + @overload async def search( self, diff --git a/src/diracx/client/models/__init__.py b/src/diracx/client/models/__init__.py index 205f5098..34678bc5 100644 --- a/src/diracx/client/models/__init__.py +++ b/src/diracx/client/models/__init__.py @@ -6,6 +6,7 @@ from ._models import BodyAuthToken from ._models import BodyAuthTokenGrantType +from ._models import GroupInfo from ._models import HTTPValidationError from ._models import InitiateDeviceFlowResponse from ._models import InsertedJob @@ -16,6 +17,7 @@ from ._models import JobSummaryParams from ._models import JobSummaryParamsSearchItem from ._models import LimitedJobStatusReturn +from ._models import Metadata from ._models import SandboxDownloadResponse from ._models import SandboxInfo from ._models import SandboxUploadResponse @@ -23,8 +25,10 @@ from ._models import SetJobStatusReturn from ._models import SortSpec from ._models import SortSpecDirection +from ._models import SupportInfo from ._models import TokenResponse from ._models import UserInfoResponse +from ._models import VOInfo from ._models import ValidationError from ._models import ValidationErrorLocItem from ._models import VectorSearchSpec @@ -48,6 +52,7 @@ __all__ = [ "BodyAuthToken", "BodyAuthTokenGrantType", + "GroupInfo", "HTTPValidationError", "InitiateDeviceFlowResponse", "InsertedJob", @@ -58,6 +63,7 @@ "JobSummaryParams", "JobSummaryParamsSearchItem", "LimitedJobStatusReturn", + "Metadata", "SandboxDownloadResponse", "SandboxInfo", "SandboxUploadResponse", @@ -65,8 +71,10 @@ "SetJobStatusReturn", "SortSpec", "SortSpecDirection", + "SupportInfo", "TokenResponse", "UserInfoResponse", + "VOInfo", "ValidationError", "ValidationErrorLocItem", "VectorSearchSpec", diff --git a/src/diracx/client/models/_models.py b/src/diracx/client/models/_models.py index 716f06f7..b8d16458 100644 --- a/src/diracx/client/models/_models.py +++ b/src/diracx/client/models/_models.py @@ -101,6 +101,32 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +class GroupInfo(_serialization.Model): + """GroupInfo. + + All required parameters must be populated in order to send to Azure. + + :ivar properties: Properties. Required. + :vartype properties: list[str] + """ + + _validation = { + "properties": {"required": True}, + } + + _attribute_map = { + "properties": {"key": "properties", "type": "[str]"}, + } + + def __init__(self, *, properties: List[str], **kwargs: Any) -> None: + """ + :keyword properties: Properties. Required. + :paramtype properties: list[str] + """ + super().__init__(**kwargs) + self.properties = properties + + class HTTPValidationError(_serialization.Model): """HTTPValidationError. @@ -509,6 +535,34 @@ def __init__( self.application_status = application_status +class Metadata(_serialization.Model): + """Metadata. + + All required parameters must be populated in order to send to Azure. + + :ivar virtual_organizations: Virtual Organizations. Required. + :vartype virtual_organizations: dict[str, ~client.models.VOInfo] + """ + + _validation = { + "virtual_organizations": {"required": True}, + } + + _attribute_map = { + "virtual_organizations": {"key": "virtual_organizations", "type": "{VOInfo}"}, + } + + def __init__( + self, *, virtual_organizations: Dict[str, "_models.VOInfo"], **kwargs: Any + ) -> None: + """ + :keyword virtual_organizations: Virtual Organizations. Required. + :paramtype virtual_organizations: dict[str, ~client.models.VOInfo] + """ + super().__init__(**kwargs) + self.virtual_organizations = virtual_organizations + + class SandboxDownloadResponse(_serialization.Model): """SandboxDownloadResponse. @@ -807,6 +861,48 @@ def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) +class SupportInfo(_serialization.Model): + """SupportInfo. + + All required parameters must be populated in order to send to Azure. + + :ivar message: Message. Required. + :vartype message: str + :ivar webpage: Webpage. Required. + :vartype webpage: str + :ivar email: Email. Required. + :vartype email: str + """ + + _validation = { + "message": {"required": True}, + "webpage": {"required": True}, + "email": {"required": True}, + } + + _attribute_map = { + "message": {"key": "message", "type": "str"}, + "webpage": {"key": "webpage", "type": "str"}, + "email": {"key": "email", "type": "str"}, + } + + def __init__( + self, *, message: str, webpage: str, email: str, **kwargs: Any + ) -> None: + """ + :keyword message: Message. Required. + :paramtype message: str + :keyword webpage: Webpage. Required. + :paramtype webpage: str + :keyword email: Email. Required. + :paramtype email: str + """ + super().__init__(**kwargs) + self.message = message + self.webpage = webpage + self.email = email + + class TokenResponse(_serialization.Model): """TokenResponse. @@ -1025,3 +1121,50 @@ def __init__( self.parameter = parameter self.operator = operator self.values = values + + +class VOInfo(_serialization.Model): + """VOInfo. + + All required parameters must be populated in order to send to Azure. + + :ivar groups: Groups. Required. + :vartype groups: dict[str, ~client.models.GroupInfo] + :ivar support: SupportInfo. Required. + :vartype support: ~client.models.SupportInfo + :ivar default_group: Default Group. Required. + :vartype default_group: str + """ + + _validation = { + "groups": {"required": True}, + "support": {"required": True}, + "default_group": {"required": True}, + } + + _attribute_map = { + "groups": {"key": "groups", "type": "{GroupInfo}"}, + "support": {"key": "support", "type": "SupportInfo"}, + "default_group": {"key": "default_group", "type": "str"}, + } + + def __init__( + self, + *, + groups: Dict[str, "_models.GroupInfo"], + support: "_models.SupportInfo", + default_group: str, + **kwargs: Any + ) -> None: + """ + :keyword groups: Groups. Required. + :paramtype groups: dict[str, ~client.models.GroupInfo] + :keyword support: SupportInfo. Required. + :paramtype support: ~client.models.SupportInfo + :keyword default_group: Default Group. Required. + :paramtype default_group: str + """ + super().__init__(**kwargs) + self.groups = groups + self.support = support + self.default_group = default_group diff --git a/src/diracx/client/operations/_operations.py b/src/diracx/client/operations/_operations.py index ceee9e24..5cb6e97a 100644 --- a/src/diracx/client/operations/_operations.py +++ b/src/diracx/client/operations/_operations.py @@ -56,6 +56,22 @@ def build_well_known_openid_configuration_request( return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) +def build_well_known_installation_metadata_request( + **kwargs: Any, +) -> HttpRequest: # pylint: disable=name-too-long + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/.well-known/dirac-metadata" + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs) + + def build_auth_do_device_flow_request(*, user_code: str, **kwargs: Any) -> HttpRequest: _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) @@ -468,6 +484,47 @@ def build_jobs_get_job_status_history_bulk_request( # pylint: disable=name-too- ) +def build_jobs_reschedule_bulk_jobs_request( + *, job_ids: List[int], **kwargs: Any +) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + _params = case_insensitive_dict(kwargs.pop("params", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/api/jobs/reschedule" + + # Construct parameters + _params["job_ids"] = _SERIALIZER.query("job_ids", job_ids, "[int]") + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest( + method="POST", url=_url, params=_params, headers=_headers, **kwargs + ) + + +def build_jobs_reschedule_single_job_request(job_id: int, **kwargs: Any) -> HttpRequest: + _headers = case_insensitive_dict(kwargs.pop("headers", {}) or {}) + + accept = _headers.pop("Accept", "application/json") + + # Construct URL + _url = "/api/jobs/{job_id}/reschedule" + path_format_arguments = { + "job_id": _SERIALIZER.url("job_id", job_id, "int"), + } + + _url: str = _format_url_section(_url, **path_format_arguments) # type: ignore + + # Construct headers + _headers["Accept"] = _SERIALIZER.header("accept", accept, "str") + + return HttpRequest(method="POST", url=_url, headers=_headers, **kwargs) + + def build_jobs_search_request( *, page: int = 0, per_page: int = 100, **kwargs: Any ) -> HttpRequest: @@ -687,6 +744,57 @@ def openid_configuration(self, **kwargs: Any) -> Any: return deserialized + @distributed_trace + def installation_metadata(self, **kwargs: Any) -> _models.Metadata: + """Installation Metadata. + + Installation Metadata. + + :return: Metadata + :rtype: ~client.models.Metadata + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[_models.Metadata] = kwargs.pop("cls", None) + + request = build_well_known_installation_metadata_request( + headers=_headers, + params=_params, + ) + request.url = self._client.format_url(request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error( + status_code=response.status_code, response=response, error_map=error_map + ) + raise HttpResponseError(response=response) + + deserialized = self._deserialize("Metadata", pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + class AuthOperations: """ @@ -2024,6 +2132,114 @@ def get_job_status_history_bulk( return deserialized + @distributed_trace + def reschedule_bulk_jobs(self, *, job_ids: List[int], **kwargs: Any) -> Any: + """Reschedule Bulk Jobs. + + Reschedule Bulk Jobs. + + :keyword job_ids: Required. + :paramtype job_ids: list[int] + :return: any + :rtype: any + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[Any] = kwargs.pop("cls", None) + + request = build_jobs_reschedule_bulk_jobs_request( + job_ids=job_ids, + headers=_headers, + params=_params, + ) + request.url = self._client.format_url(request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error( + status_code=response.status_code, response=response, error_map=error_map + ) + raise HttpResponseError(response=response) + + deserialized = self._deserialize("object", pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + + @distributed_trace + def reschedule_single_job(self, job_id: int, **kwargs: Any) -> Any: + """Reschedule Single Job. + + Reschedule Single Job. + + :param job_id: Required. + :type job_id: int + :return: any + :rtype: any + :raises ~azure.core.exceptions.HttpResponseError: + """ + error_map = { + 401: ClientAuthenticationError, + 404: ResourceNotFoundError, + 409: ResourceExistsError, + 304: ResourceNotModifiedError, + } + error_map.update(kwargs.pop("error_map", {}) or {}) + + _headers = kwargs.pop("headers", {}) or {} + _params = kwargs.pop("params", {}) or {} + + cls: ClsType[Any] = kwargs.pop("cls", None) + + request = build_jobs_reschedule_single_job_request( + job_id=job_id, + headers=_headers, + params=_params, + ) + request.url = self._client.format_url(request.url) + + _stream = False + pipeline_response: PipelineResponse = ( + self._client._pipeline.run( # pylint: disable=protected-access + request, stream=_stream, **kwargs + ) + ) + + response = pipeline_response.http_response + + if response.status_code not in [200]: + map_error( + status_code=response.status_code, response=response, error_map=error_map + ) + raise HttpResponseError(response=response) + + deserialized = self._deserialize("object", pipeline_response) + + if cls: + return cls(pipeline_response, deserialized, {}) + + return deserialized + @overload def search( self, diff --git a/src/diracx/core/config/schema.py b/src/diracx/core/config/schema.py index e7b78b5e..57d1f302 100644 --- a/src/diracx/core/config/schema.py +++ b/src/diracx/core/config/schema.py @@ -5,7 +5,7 @@ from typing import Any, Optional from pydantic import BaseModel as _BaseModel -from pydantic import EmailStr, PrivateAttr, root_validator +from pydantic import EmailStr, Field, PrivateAttr, root_validator from ..properties import SecurityProperty @@ -67,8 +67,15 @@ def server_metadata_url(self): return f"{self.URL}/.well-known/openid-configuration" +class SupportInfo(BaseModel): + Email: str | None = None + Webpage: str | None = None + Message: str = "Please contact system administrator" + + class RegistryConfig(BaseModel): IdP: IdpConfig + Support: SupportInfo = Field(default_factory=SupportInfo) DefaultGroup: str DefaultStorageQuota: float = 0 DefaultProxyLifeTime: int = 12 * 60 * 60 diff --git a/src/diracx/routers/well_known.py b/src/diracx/routers/well_known.py index 9f7b93f1..6bd5f0f7 100644 --- a/src/diracx/routers/well_known.py +++ b/src/diracx/routers/well_known.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import TypedDict + from fastapi import Request from diracx.routers.auth import AuthSettings @@ -40,3 +42,46 @@ async def openid_configuration( "token_endpoint_auth_methods_supported": ["none"], "code_challenge_methods_supported": ["S256"], } + + +class SupportInfo(TypedDict): + message: str + webpage: str | None + email: str | None + + +class GroupInfo(TypedDict): + properties: list[str] + + +class VOInfo(TypedDict): + groups: dict[str, GroupInfo] + support: SupportInfo + default_group: str + + +class Metadata(TypedDict): + virtual_organizations: dict[str, VOInfo] + + +@router.get("/dirac-metadata") +async def installation_metadata(config: Config) -> Metadata: + metadata: Metadata = { + "virtual_organizations": {}, + } + for vo, vo_info in config.Registry.items(): + groups: dict[str, GroupInfo] = { + group: {"properties": sorted(group_info.Properties)} + for group, group_info in vo_info.Groups.items() + } + metadata["virtual_organizations"][vo] = { + "groups": groups, + "support": { + "message": vo_info.Support.Message, + "webpage": vo_info.Support.Webpage, + "email": vo_info.Support.Email, + }, + "default_group": vo_info.DefaultGroup, + } + + return metadata diff --git a/tests/cli/legacy/cs_sync/convert_integration_test.yaml b/tests/cli/legacy/cs_sync/convert_integration_test.yaml index 3c31d0a2..3da51e61 100644 --- a/tests/cli/legacy/cs_sync/convert_integration_test.yaml +++ b/tests/cli/legacy/cs_sync/convert_integration_test.yaml @@ -8,6 +8,10 @@ VOs: adminusername: e2cb28ec-1a1e-40ee-a56d-d899b79879ce ciuser: 26dbe36e-cf5c-4c52-a834-29a1c904ef74 trialUser: a95ab678-3fa4-41b9-b863-fe62ce8064ce + Support: + Message: "Contact the help desk" + Email: "helpdesk@example.invalid" + Webpage: "https://helpdesk.vo.invalid" vo: DefaultGroup: dirac_user IdP: diff --git a/tests/cli/legacy/cs_sync/integration_test.yaml b/tests/cli/legacy/cs_sync/integration_test.yaml index ec54e334..abc889f1 100644 --- a/tests/cli/legacy/cs_sync/integration_test.yaml +++ b/tests/cli/legacy/cs_sync/integration_test.yaml @@ -69,6 +69,10 @@ Registry: IdP: ClientID: 995ed3b9-d5bd-49d3-a7f4-7fc7dbd5a0cd URL: https://jenkins.invalid/ + Support: + Email: "helpdesk@example.invalid" + Message: "Contact the help desk" + Webpage: "https://helpdesk.vo.invalid" Users: 26dbe36e-cf5c-4c52-a834-29a1c904ef74: Email: lhcb-dirac-ci@cern.ch @@ -103,6 +107,7 @@ Registry: IdP: ClientID: 072afab5-ed92-46e0-a61d-4ecbc96e0770 URL: https://vo.invalid/ + Support: {} Users: 26b14fc9-6d40-4ca5-b014-6234eaf0fb6e: Email: lhcb-dirac-ci@cern.ch diff --git a/tests/cli/legacy/cs_sync/test_cssync.py b/tests/cli/legacy/cs_sync/test_cssync.py index 4cfa179a..3c3c5a22 100644 --- a/tests/cli/legacy/cs_sync/test_cssync.py +++ b/tests/cli/legacy/cs_sync/test_cssync.py @@ -4,6 +4,7 @@ from typer.testing import CliRunner from diracx.cli import app +from diracx.core.config.schema import Config runner = CliRunner() @@ -28,7 +29,7 @@ def test_cs_sync(tmp_path, monkeypatch): ) assert result.exit_code == 0 assert output_file.is_file() - actual_output = yaml.safe_load(output_file.read_text()) expected_output = yaml.safe_load((file_path / "integration_test.yaml").read_text()) assert actual_output == expected_output + Config.parse_obj(actual_output) diff --git a/tests/routers/test_generic.py b/tests/routers/test_generic.py index d34b12cd..4fe306d9 100644 --- a/tests/routers/test_generic.py +++ b/tests/routers/test_generic.py @@ -8,3 +8,10 @@ def test_oidc_configuration(test_client): r = test_client.get("/.well-known/openid-configuration") assert r.status_code == 200 assert r.json() + + +def test_installation_metadata(test_client): + r = test_client.get("/.well-known/dirac-metadata") + + assert r.status_code == 200 + assert r.json()